From b21d2a685e100ad3ad75d7b1b152662e5e9c4e33 Mon Sep 17 00:00:00 2001 From: Eauldane Date: Fri, 22 Aug 2025 11:55:35 +0100 Subject: [PATCH] Initial --- .dockerignore | 284 ++++ .gitignore | 357 +++++ .gitmodules | 3 + Docker/Readme.md | 39 + .../Dockerfile-MareSynchronosAuthService | 32 + .../Dockerfile-MareSynchronosAuthService-git | 30 + Docker/build/Dockerfile-MareSynchronosServer | 32 + .../build/Dockerfile-MareSynchronosServer-git | 30 + .../build/Dockerfile-MareSynchronosServices | 32 + .../Dockerfile-MareSynchronosServices-git | 30 + ...Dockerfile-MareSynchronosStaticFilesServer | 32 + ...erfile-MareSynchronosStaticFilesServer-git | 30 + .../linux-git/docker-build-authservice.sh | 2 + Docker/build/linux-git/docker-build-server.sh | 2 + .../build/linux-git/docker-build-services.sh | 2 + .../docker-build-staticfilesserver.sh | 2 + Docker/build/linux-git/docker-build.sh | 5 + .../linux-local/docker-build-authservice.sh | 2 + .../build/linux-local/docker-build-server.sh | 4 + .../linux-local/docker-build-services.sh | 4 + .../docker-build-staticfilesserver.sh | 4 + Docker/build/linux-local/docker-build.sh | 5 + .../windows-git/docker-build-authservice.bat | 3 + .../build/windows-git/docker-build-server.bat | 2 + .../windows-git/docker-build-services.bat | 3 + .../docker-build-staticfilesserver.bat | 3 + Docker/build/windows-git/docker-build.bat | 6 + .../docker-build-authservice.bat | 4 + .../windows-local/docker-build-server.bat | 4 + .../windows-local/docker-build-services.bat | 4 + .../docker-build-staticfilesserver.bat | 4 + Docker/build/windows-local/docker-build.bat | 6 + Docker/run/compose/mare-sharded.yml | 145 ++ Docker/run/compose/mare-standalone.yml | 102 ++ Docker/run/config/sharded/files-shard-1.json | 53 + Docker/run/config/sharded/files-shard-2.json | 53 + .../run/config/sharded/files-shard-main.json | 56 + Docker/run/config/sharded/haproxy-shards.cfg | 30 + Docker/run/config/sharded/server-shard-1.json | 45 + Docker/run/config/sharded/server-shard-2.json | 45 + .../run/config/sharded/server-shard-main.json | 71 + .../standalone/authservice-standalone.json | 42 + .../config/standalone/files-standalone.json | 53 + .../config/standalone/server-standalone.json | 57 + .../standalone/services-standalone.json | 44 + Docker/run/linux-sharded-daemon-start.sh | 2 + Docker/run/linux-sharded-daemon-stop.sh | 2 + Docker/run/linux-sharded.sh | 2 + Docker/run/linux-standalone-daemon-start.sh | 2 + Docker/run/linux-standalone-daemon-stop.sh | 2 + Docker/run/linux-standalone.sh | 2 + Docker/run/windows-sharded-daemon-start.bat | 2 + Docker/run/windows-sharded-daemon-stop.bat | 2 + Docker/run/windows-sharded.bat | 2 + .../run/windows-standalone-daemon-start.bat | 2 + Docker/run/windows-standalone-daemon-stop.bat | 2 + Docker/run/windows-standalone.bat | 2 + LICENSE | 21 + MareAPI/.gitignore | 350 +++++ MareAPI/LICENSE | 21 + .../MareSynchronosAPI/Data/CharacterData.cs | 36 + MareAPI/MareSynchronosAPI/Data/ChatMessage.cs | 11 + .../Data/Comparer/GroupDataComparer.cs | 19 + .../Data/Comparer/GroupDtoComparer.cs | 23 + .../Data/Comparer/GroupPairDtoComparer.cs | 20 + .../Data/Comparer/UserDataComparer.cs | 20 + .../Data/Comparer/UserDtoComparer.cs | 20 + .../Data/Enum/GroupPermissions.cs | 11 + .../Data/Enum/GroupUserInfo.cs | 9 + .../Data/Enum/GroupUserPermissions.cs | 11 + .../Data/Enum/MessageSeverity.cs | 8 + .../MareSynchronosAPI/Data/Enum/ObjectKind.cs | 9 + .../Data/Enum/UserPermissions.cs | 12 + .../Extensions/GroupPermissionsExtensions.cs | 50 + .../Extensions/GroupUserInfoExtensions.cs | 28 + .../GroupUserPermissionsExtensions.cs | 50 + .../Extensions/UserPermissionsExtensions.cs | 61 + .../Data/FileReplacementData.cs | 30 + MareAPI/MareSynchronosAPI/Data/GroupData.cs | 10 + .../Data/SignedChatMessage.cs | 14 + MareAPI/MareSynchronosAPI/Data/UserData.cs | 10 + .../Dto/Account/RegisterReplyDto.cs | 12 + .../Dto/Account/RegisterReplyV2Dto.cs | 11 + MareAPI/MareSynchronosAPI/Dto/AuthReplyDto.cs | 11 + .../Dto/CharaData/AccessTypeDto.cs | 9 + .../Dto/CharaData/CharaDataDownloadDto.cs | 14 + .../Dto/CharaData/CharaDataDto.cs | 9 + .../Dto/CharaData/CharaDataFullDto.cs | 88 ++ .../Dto/CharaData/CharaDataMetaInfoDto.cs | 11 + .../Dto/CharaData/CharaDataUpdateDto.cs | 20 + .../Dto/CharaData/ShareTypeDto.cs | 7 + .../Dto/Chat/GroupChatMsgDto.cs | 13 + .../Dto/Chat/UserChatMsgDto.cs | 11 + .../MareSynchronosAPI/Dto/ConnectionDto.cs | 25 + .../Dto/Files/DownloadFileDto.cs | 14 + .../Dto/Files/FilesSendDto.cs | 13 + .../Dto/Files/ITransferFileDto.cs | 8 + .../Dto/Files/UploadFileDto.cs | 11 + .../Dto/Group/BannedGroupUserDto.cs | 19 + .../MareSynchronosAPI/Dto/Group/GroupDto.cs | 13 + .../Dto/Group/GroupFullInfoDto.cs | 12 + .../Dto/Group/GroupInfoDto.cs | 16 + .../Dto/Group/GroupPairDto.cs | 12 + .../Dto/Group/GroupPairFullInfoDto.cs | 12 + .../Dto/Group/GroupPairUserInfoDto.cs | 8 + .../Dto/Group/GroupPairUserPermissionDto.cs | 8 + .../Dto/Group/GroupPasswordDto.cs | 7 + .../Dto/Group/GroupPermissionDto.cs | 8 + .../MareSynchronosAPI/Dto/SystemInfoDto.cs | 9 + .../Dto/User/OnlineUserCharaDataDto.cs | 7 + .../Dto/User/OnlineUserIdentDto.cs | 7 + .../Dto/User/UserCharaDataMessageDto.cs | 7 + MareAPI/MareSynchronosAPI/Dto/User/UserDto.cs | 7 + .../MareSynchronosAPI/Dto/User/UserPairDto.cs | 12 + .../Dto/User/UserPermissionsDto.cs | 8 + .../Dto/User/UserProfileDto.cs | 7 + .../Dto/User/UserProfileReportDto.cs | 7 + .../MareSynchronos.API.csproj | 13 + .../MareSynchronosAPI/MareSynchronosAPI.sln | 25 + MareAPI/MareSynchronosAPI/Routes/MareAuth.cs | 14 + MareAPI/MareSynchronosAPI/Routes/MareFiles.cs | 45 + MareAPI/MareSynchronosAPI/SignalR/IMareHub.cs | 144 ++ .../SignalR/IMareHubClient.cs | 62 + MareSynchronosServer/.editorconfig | 4 + .../Authentication/SecretKeyAuthReply.cs | 3 + .../SecretKeyFailedAuthorization.cs | 12 + .../Controllers/JwtController.cs | 193 +++ .../MareSynchronosAuthService.csproj | 38 + .../MareSynchronosAuthService/Program.cs | 40 + .../Properties/launchSettings.json | 29 + .../Services/AccountRegistrationService.cs | 152 ++ .../Services/GeoIPService.cs | 138 ++ .../Services/SecretKeyAuthenticatorService.cs | 88 ++ .../MareSynchronosAuthService/Startup.cs | 226 +++ .../appsettings.Development.json | 8 + .../appsettings.json | 9 + MareSynchronosServer/MareSynchronosServer.sln | 60 + .../.config/dotnet-tools.json | 12 + .../Controllers/ClientMessageController.cs | 42 + .../Controllers/MainController.cs | 31 + .../Hubs/MareHub.CharaData.cs | 638 +++++++++ .../MareSynchronosServer/Hubs/MareHub.Chat.cs | 52 + .../Hubs/MareHub.ClientStubs.cs | 63 + .../Hubs/MareHub.Functions.cs | 295 ++++ .../Hubs/MareHub.GposeLobby.cs | 155 ++ .../Hubs/MareHub.Groups.cs | 562 ++++++++ .../MareSynchronosServer/Hubs/MareHub.User.cs | 514 +++++++ .../MareSynchronosServer/Hubs/MareHub.cs | 147 ++ .../Hubs/SignalRLimitFilter.cs | 112 ++ .../MareSynchronosServer.csproj | 42 + .../MareSynchronosServer/Program.cs | 77 + .../Properties/launchSettings.json | 14 + .../Properties/serviceDependencies.json | 3 + .../Properties/serviceDependencies.local.json | 3 + .../Services/CharaDataCleanupService.cs | 50 + .../Services/GPoseLobbyDistributionService.cs | 226 +++ .../Services/SystemInfoService.cs | 98 ++ .../Services/UserCleanupService.cs | 195 +++ .../MareSynchronosServer/Startup.cs | 343 +++++ .../MareSynchronosServer/Utils/Extensions.cs | 53 + .../Utils/MareHubLogger.cs | 33 + .../MareSynchronosServer/Utils/PauseInfo.cs | 8 + .../MareSynchronosServer/Utils/PauseState.cs | 9 + .../MareSynchronosServer/Utils/PausedEntry.cs | 58 + .../MareSynchronosServer/Utils/UserPair.cs | 12 + .../appsettings.Development.json | 10 + .../MareSynchronosServer/appsettings.json | 61 + .../Discord/DiscordBotTest.cs | 53 + .../Hubs/MareHubTest.cs | 82 ++ .../MareSynchronosServerTest.csproj | 26 + .../MareSynchronosServerTest/Usings.cs | 1 + .../.config/dotnet-tools.json | 12 + .../Discord/DiscordBot.cs | 421 ++++++ .../Discord/DiscordBotServices.cs | 61 + .../Discord/MareModule.cs | 1260 +++++++++++++++++ .../MareSynchronosServices/DummyHub.cs | 25 + .../MareSynchronosServices.csproj | 38 + .../MareSynchronosServices/Program.cs | 43 + .../Properties/launchSettings.json | 13 + .../MareSynchronosServices/Startup.cs | 104 ++ .../appsettings.Development.json | 8 + .../MareSynchronosServices/appsettings.json | 29 + .../Data/MareDbContext.cs | 131 ++ .../MareSynchronosShared/Extensions.cs | 39 + .../MareSynchronosShared.csproj | 59 + .../Metrics/MareMetrics.cs | 84 ++ .../Metrics/MetricsAPI.cs | 46 + .../20220731210149_InitialCreate.Designer.cs | 241 ++++ .../20220731210149_InitialCreate.cs | 163 +++ ...731211419_RenameLowerSnakeCase.Designer.cs | 241 ++++ .../20220731211419_RenameLowerSnakeCase.cs | 133 ++ ...0220801121419_AddLodestoneAuth.Designer.cs | 283 ++++ .../20220801121419_AddLodestoneAuth.cs | 43 + ...ullableLodestoneAuthProperties.Designer.cs | 283 ++++ ...2103_AddNullableLodestoneAuthProperties.cs | 33 + ...6103053_AddBannedRegistrations.Designer.cs | 295 ++++ .../20220806103053_AddBannedRegistrations.cs | 29 + ...16170426_SetMaxLimitForStrings.Designer.cs | 302 ++++ .../20220816170426_SetMaxLimitForStrings.cs | 131 ++ .../20220824225157_AddAlias.Designer.cs | 307 ++++ .../Migrations/20220824225157_AddAlias.cs | 26 + .../20220917115233_Groups.Designer.cs | 389 +++++ .../Migrations/20220917115233_Groups.cs | 123 ++ ...20220929150304_ChangeGidLength.Designer.cs | 389 +++++ .../20220929150304_ChangeGidLength.cs | 51 + .../20221002105428_IsPinned.Designer.cs | 393 +++++ .../Migrations/20221002105428_IsPinned.cs | 26 + ...221004125939_AdjustAliasLength.Designer.cs | 393 +++++ .../20221004125939_AdjustAliasLength.cs | 37 + .../20221006115929_GroupModerator.Designer.cs | 397 ++++++ .../20221006115929_GroupModerator.cs | 26 + .../20221006122618_groupbans.Designer.cs | 462 ++++++ .../Migrations/20221006122618_groupbans.cs | 135 ++ ...20221024141548_GroupTempInvite.Designer.cs | 501 +++++++ .../20221024141548_GroupTempInvite.cs | 47 + ...21024181912_AdjustInviteLength.Designer.cs | 501 +++++++ .../20221024181912_AdjustInviteLength.cs | 35 + .../20221228033214_FileCacheSize.Designer.cs | 506 +++++++ .../20221228033214_FileCacheSize.cs | 29 + ...20230111092127_IsBannedForAuth.Designer.cs | 510 +++++++ .../20230111092127_IsBannedForAuth.cs | 29 + ...20230118184347_FilesUploadDate.Designer.cs | 514 +++++++ .../20230118184347_FilesUploadDate.cs | 30 + .../20230126163758_GroupPerms.Designer.cs | 530 +++++++ .../Migrations/20230126163758_GroupPerms.cs | 62 + ...131193425_AddPrimaryUserToAuth.Designer.cs | 544 +++++++ .../20230131193425_AddPrimaryUserToAuth.cs | 210 +++ .../20230228001033_UserPerms.Designer.cs | 552 ++++++++ .../Migrations/20230228001033_UserPerms.cs | 40 + ...20230319015307_UserProfileData.Designer.cs | 584 ++++++++ .../20230319015307_UserProfileData.cs | 40 + ...30319114005_UserProfileReports.Designer.cs | 650 +++++++++ .../20230319114005_UserProfileReports.cs | 92 ++ .../20230420075153_DisableVFX.Designer.cs | 662 +++++++++ .../Migrations/20230420075153_DisableVFX.cs | 51 + .../20250627174017_Bump.Designer.cs | 662 +++++++++ .../Migrations/20250627174017_Bump.cs | 250 ++++ .../20250627174541_MCDO.Designer.cs | 974 +++++++++++++ .../Migrations/20250627174541_MCDO.cs | 225 +++ .../20250627204223_AllowedGroup.Designer.cs | 1094 ++++++++++++++ .../Migrations/20250627204223_AllowedGroup.cs | 98 ++ .../Migrations/MareDbContextModelSnapshot.cs | 992 +++++++++++++ .../MareSynchronosShared/Models/Auth.cs | 16 + .../MareSynchronosShared/Models/Banned.cs | 13 + .../Models/BannedRegistrations.cs | 10 + .../MareSynchronosShared/Models/CharaData.cs | 91 ++ .../MareSynchronosShared/Models/ClientPair.cs | 20 + .../MareSynchronosShared/Models/FileCache.cs | 18 + .../Models/ForbiddenUploadEntry.cs | 14 + .../MareSynchronosShared/Models/Group.cs | 19 + .../MareSynchronosShared/Models/GroupBan.cs | 13 + .../MareSynchronosShared/Models/GroupPair.cs | 15 + .../Models/GroupTempInvite.cs | 12 + .../Models/LodeStoneAuth.cs | 15 + .../MareSynchronosShared/Models/User.cs | 20 + .../Models/UserProfileData.cs | 20 + .../Models/UserProfileDataReport.cs | 24 + .../RequirementHandlers/UserRequirement.cs | 13 + .../UserRequirementHandler.cs | 55 + .../RequirementHandlers/UserRequirements.cs | 8 + .../Services/IConfigurationService.cs | 11 + .../Services/MareConfigurationController.cs | 60 + .../MareConfigurationServiceClient.cs | 189 +++ .../MareConfigurationServiceServer.cs | 51 + .../AllowedControllersFeatureProvider.cs | 22 + .../Utils/ClientMessage.cs | 4 + .../Configuration/AuthServiceConfiguration.cs | 29 + .../Configuration/CdnShardConfiguration.cs | 13 + .../Utils/Configuration/IMareConfiguration.cs | 8 + .../Configuration/MareConfigurationBase.cs | 51 + .../Configuration/ServerConfiguration.cs | 46 + .../Configuration/ServicesConfiguration.cs | 21 + .../StaticFilesServerConfiguration.cs | 67 + .../Utils/IdBasedUserIdProvider.cs | 11 + .../Utils/MareClaimTypes.cs | 10 + .../Utils/RemoteConfigurationAttribute.cs | 4 + .../Utils/ServerTokenGenerator.cs | 61 + .../Utils/SharedDbFunctions.cs | 124 ++ .../MareSynchronosShared/Utils/StringUtils.cs | 49 + .../.config/dotnet-tools.json | 12 + .../Controllers/CacheController.cs | 51 + .../Controllers/ControllerBase.cs | 18 + .../Controllers/DistributionController.cs | 60 + .../Controllers/MainController.cs | 25 + .../Controllers/RequestController.cs | 72 + .../Controllers/ServerFilesController.cs | 280 ++++ .../DummyHub.cs | 25 + .../MareSynchronosStaticFilesServer.csproj | 38 + .../Program.cs | 43 + .../Properties/launchSettings.json | 28 + .../Services/CachedFileProvider.cs | 243 ++++ .../Services/ColdTouchHashService.cs | 82 ++ .../Services/FileCleanupService.cs | 359 +++++ .../Services/FilePreFetchService.cs | 125 ++ .../Services/FileStatisticsService.cs | 94 ++ .../Services/IClientReadyMessageService.cs | 6 + .../Services/ITouchHashService.cs | 6 + .../Services/MainClientReadyMessageService.cs | 26 + .../Services/RequestQueueService.cs | 222 +++ .../ShardClientReadyMessageService.cs | 51 + .../Services/ShardTouchMessageService.cs | 132 ++ .../Startup.cs | 271 ++++ .../Utils/CountedStream.cs | 73 + .../Utils/FilePathUtil.cs | 31 + .../Utils/HashingStream.cs | 82 ++ .../Utils/RequestBlockFileListResult.cs | 76 + .../RequestBlockFileListResultFactory.cs | 25 + .../Utils/TeeStream.cs | 74 + .../Utils/UserQueueEntry.cs | 21 + .../Utils/UserRequest.cs | 6 + .../appsettings.Development.json | 9 + .../appsettings.json | 31 + 312 files changed, 31174 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 Docker/Readme.md create mode 100644 Docker/build/Dockerfile-MareSynchronosAuthService create mode 100644 Docker/build/Dockerfile-MareSynchronosAuthService-git create mode 100644 Docker/build/Dockerfile-MareSynchronosServer create mode 100644 Docker/build/Dockerfile-MareSynchronosServer-git create mode 100644 Docker/build/Dockerfile-MareSynchronosServices create mode 100644 Docker/build/Dockerfile-MareSynchronosServices-git create mode 100644 Docker/build/Dockerfile-MareSynchronosStaticFilesServer create mode 100644 Docker/build/Dockerfile-MareSynchronosStaticFilesServer-git create mode 100644 Docker/build/linux-git/docker-build-authservice.sh create mode 100644 Docker/build/linux-git/docker-build-server.sh create mode 100644 Docker/build/linux-git/docker-build-services.sh create mode 100644 Docker/build/linux-git/docker-build-staticfilesserver.sh create mode 100644 Docker/build/linux-git/docker-build.sh create mode 100644 Docker/build/linux-local/docker-build-authservice.sh create mode 100644 Docker/build/linux-local/docker-build-server.sh create mode 100644 Docker/build/linux-local/docker-build-services.sh create mode 100644 Docker/build/linux-local/docker-build-staticfilesserver.sh create mode 100644 Docker/build/linux-local/docker-build.sh create mode 100644 Docker/build/windows-git/docker-build-authservice.bat create mode 100644 Docker/build/windows-git/docker-build-server.bat create mode 100644 Docker/build/windows-git/docker-build-services.bat create mode 100644 Docker/build/windows-git/docker-build-staticfilesserver.bat create mode 100644 Docker/build/windows-git/docker-build.bat create mode 100644 Docker/build/windows-local/docker-build-authservice.bat create mode 100644 Docker/build/windows-local/docker-build-server.bat create mode 100644 Docker/build/windows-local/docker-build-services.bat create mode 100644 Docker/build/windows-local/docker-build-staticfilesserver.bat create mode 100644 Docker/build/windows-local/docker-build.bat create mode 100644 Docker/run/compose/mare-sharded.yml create mode 100644 Docker/run/compose/mare-standalone.yml create mode 100644 Docker/run/config/sharded/files-shard-1.json create mode 100644 Docker/run/config/sharded/files-shard-2.json create mode 100644 Docker/run/config/sharded/files-shard-main.json create mode 100644 Docker/run/config/sharded/haproxy-shards.cfg create mode 100644 Docker/run/config/sharded/server-shard-1.json create mode 100644 Docker/run/config/sharded/server-shard-2.json create mode 100644 Docker/run/config/sharded/server-shard-main.json create mode 100644 Docker/run/config/standalone/authservice-standalone.json create mode 100644 Docker/run/config/standalone/files-standalone.json create mode 100644 Docker/run/config/standalone/server-standalone.json create mode 100644 Docker/run/config/standalone/services-standalone.json create mode 100644 Docker/run/linux-sharded-daemon-start.sh create mode 100644 Docker/run/linux-sharded-daemon-stop.sh create mode 100644 Docker/run/linux-sharded.sh create mode 100644 Docker/run/linux-standalone-daemon-start.sh create mode 100644 Docker/run/linux-standalone-daemon-stop.sh create mode 100644 Docker/run/linux-standalone.sh create mode 100644 Docker/run/windows-sharded-daemon-start.bat create mode 100644 Docker/run/windows-sharded-daemon-stop.bat create mode 100644 Docker/run/windows-sharded.bat create mode 100644 Docker/run/windows-standalone-daemon-start.bat create mode 100644 Docker/run/windows-standalone-daemon-stop.bat create mode 100644 Docker/run/windows-standalone.bat create mode 100644 LICENSE create mode 100644 MareAPI/.gitignore create mode 100644 MareAPI/LICENSE create mode 100644 MareAPI/MareSynchronosAPI/Data/CharacterData.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/ChatMessage.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Comparer/GroupDataComparer.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Comparer/GroupDtoComparer.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Comparer/GroupPairDtoComparer.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Comparer/UserDataComparer.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Comparer/UserDtoComparer.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Enum/GroupPermissions.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Enum/GroupUserInfo.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Enum/GroupUserPermissions.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Enum/MessageSeverity.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Enum/ObjectKind.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Enum/UserPermissions.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Extensions/GroupPermissionsExtensions.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Extensions/GroupUserInfoExtensions.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Extensions/GroupUserPermissionsExtensions.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Extensions/UserPermissionsExtensions.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/FileReplacementData.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/GroupData.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/SignedChatMessage.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/UserData.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Account/RegisterReplyDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Account/RegisterReplyV2Dto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/AuthReplyDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/CharaData/AccessTypeDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataDownloadDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataFullDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataMetaInfoDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataUpdateDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/CharaData/ShareTypeDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Chat/GroupChatMsgDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Chat/UserChatMsgDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/ConnectionDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Files/DownloadFileDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Files/FilesSendDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Files/ITransferFileDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Files/UploadFileDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Group/BannedGroupUserDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Group/GroupDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Group/GroupFullInfoDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Group/GroupInfoDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Group/GroupPairDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Group/GroupPairFullInfoDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Group/GroupPairUserInfoDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Group/GroupPairUserPermissionDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Group/GroupPasswordDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Group/GroupPermissionDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/SystemInfoDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/User/OnlineUserCharaDataDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/User/OnlineUserIdentDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/User/UserCharaDataMessageDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/User/UserDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/User/UserPairDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/User/UserPermissionsDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/User/UserProfileDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/User/UserProfileReportDto.cs create mode 100644 MareAPI/MareSynchronosAPI/MareSynchronos.API.csproj create mode 100644 MareAPI/MareSynchronosAPI/MareSynchronosAPI.sln create mode 100644 MareAPI/MareSynchronosAPI/Routes/MareAuth.cs create mode 100644 MareAPI/MareSynchronosAPI/Routes/MareFiles.cs create mode 100644 MareAPI/MareSynchronosAPI/SignalR/IMareHub.cs create mode 100644 MareAPI/MareSynchronosAPI/SignalR/IMareHubClient.cs create mode 100644 MareSynchronosServer/.editorconfig create mode 100644 MareSynchronosServer/MareSynchronosAuthService/Authentication/SecretKeyAuthReply.cs create mode 100644 MareSynchronosServer/MareSynchronosAuthService/Authentication/SecretKeyFailedAuthorization.cs create mode 100644 MareSynchronosServer/MareSynchronosAuthService/Controllers/JwtController.cs create mode 100644 MareSynchronosServer/MareSynchronosAuthService/MareSynchronosAuthService.csproj create mode 100644 MareSynchronosServer/MareSynchronosAuthService/Program.cs create mode 100644 MareSynchronosServer/MareSynchronosAuthService/Properties/launchSettings.json create mode 100644 MareSynchronosServer/MareSynchronosAuthService/Services/AccountRegistrationService.cs create mode 100644 MareSynchronosServer/MareSynchronosAuthService/Services/GeoIPService.cs create mode 100644 MareSynchronosServer/MareSynchronosAuthService/Services/SecretKeyAuthenticatorService.cs create mode 100644 MareSynchronosServer/MareSynchronosAuthService/Startup.cs create mode 100644 MareSynchronosServer/MareSynchronosAuthService/appsettings.Development.json create mode 100644 MareSynchronosServer/MareSynchronosAuthService/appsettings.json create mode 100644 MareSynchronosServer/MareSynchronosServer.sln create mode 100644 MareSynchronosServer/MareSynchronosServer/.config/dotnet-tools.json create mode 100644 MareSynchronosServer/MareSynchronosServer/Controllers/ClientMessageController.cs create mode 100644 MareSynchronosServer/MareSynchronosServer/Controllers/MainController.cs create mode 100644 MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.CharaData.cs create mode 100644 MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Chat.cs create mode 100644 MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.ClientStubs.cs create mode 100644 MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Functions.cs create mode 100644 MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.GposeLobby.cs create mode 100644 MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Groups.cs create mode 100644 MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.User.cs create mode 100644 MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.cs create mode 100644 MareSynchronosServer/MareSynchronosServer/Hubs/SignalRLimitFilter.cs create mode 100644 MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj create mode 100644 MareSynchronosServer/MareSynchronosServer/Program.cs create mode 100644 MareSynchronosServer/MareSynchronosServer/Properties/launchSettings.json create mode 100644 MareSynchronosServer/MareSynchronosServer/Properties/serviceDependencies.json create mode 100644 MareSynchronosServer/MareSynchronosServer/Properties/serviceDependencies.local.json create mode 100644 MareSynchronosServer/MareSynchronosServer/Services/CharaDataCleanupService.cs create mode 100644 MareSynchronosServer/MareSynchronosServer/Services/GPoseLobbyDistributionService.cs create mode 100644 MareSynchronosServer/MareSynchronosServer/Services/SystemInfoService.cs create mode 100644 MareSynchronosServer/MareSynchronosServer/Services/UserCleanupService.cs create mode 100644 MareSynchronosServer/MareSynchronosServer/Startup.cs create mode 100644 MareSynchronosServer/MareSynchronosServer/Utils/Extensions.cs create mode 100644 MareSynchronosServer/MareSynchronosServer/Utils/MareHubLogger.cs create mode 100644 MareSynchronosServer/MareSynchronosServer/Utils/PauseInfo.cs create mode 100644 MareSynchronosServer/MareSynchronosServer/Utils/PauseState.cs create mode 100644 MareSynchronosServer/MareSynchronosServer/Utils/PausedEntry.cs create mode 100644 MareSynchronosServer/MareSynchronosServer/Utils/UserPair.cs create mode 100644 MareSynchronosServer/MareSynchronosServer/appsettings.Development.json create mode 100644 MareSynchronosServer/MareSynchronosServer/appsettings.json create mode 100644 MareSynchronosServer/MareSynchronosServerTest/Discord/DiscordBotTest.cs create mode 100644 MareSynchronosServer/MareSynchronosServerTest/Hubs/MareHubTest.cs create mode 100644 MareSynchronosServer/MareSynchronosServerTest/MareSynchronosServerTest.csproj create mode 100644 MareSynchronosServer/MareSynchronosServerTest/Usings.cs create mode 100644 MareSynchronosServer/MareSynchronosServices/.config/dotnet-tools.json create mode 100644 MareSynchronosServer/MareSynchronosServices/Discord/DiscordBot.cs create mode 100644 MareSynchronosServer/MareSynchronosServices/Discord/DiscordBotServices.cs create mode 100644 MareSynchronosServer/MareSynchronosServices/Discord/MareModule.cs create mode 100644 MareSynchronosServer/MareSynchronosServices/DummyHub.cs create mode 100644 MareSynchronosServer/MareSynchronosServices/MareSynchronosServices.csproj create mode 100644 MareSynchronosServer/MareSynchronosServices/Program.cs create mode 100644 MareSynchronosServer/MareSynchronosServices/Properties/launchSettings.json create mode 100644 MareSynchronosServer/MareSynchronosServices/Startup.cs create mode 100644 MareSynchronosServer/MareSynchronosServices/appsettings.Development.json create mode 100644 MareSynchronosServer/MareSynchronosServices/appsettings.json create mode 100644 MareSynchronosServer/MareSynchronosShared/Data/MareDbContext.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Extensions.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/MareSynchronosShared.csproj create mode 100644 MareSynchronosServer/MareSynchronosShared/Metrics/MareMetrics.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Metrics/MetricsAPI.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20220731210149_InitialCreate.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20220731210149_InitialCreate.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20220731211419_RenameLowerSnakeCase.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20220731211419_RenameLowerSnakeCase.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20220801121419_AddLodestoneAuth.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20220801121419_AddLodestoneAuth.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20220806103053_AddBannedRegistrations.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20220806103053_AddBannedRegistrations.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20220816170426_SetMaxLimitForStrings.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20220816170426_SetMaxLimitForStrings.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20220824225157_AddAlias.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20220824225157_AddAlias.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20220917115233_Groups.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20220917115233_Groups.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20220929150304_ChangeGidLength.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20220929150304_ChangeGidLength.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20221002105428_IsPinned.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20221002105428_IsPinned.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20221004125939_AdjustAliasLength.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20221004125939_AdjustAliasLength.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20221006115929_GroupModerator.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20221006115929_GroupModerator.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20221006122618_groupbans.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20221006122618_groupbans.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20221024141548_GroupTempInvite.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20221024141548_GroupTempInvite.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20221024181912_AdjustInviteLength.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20221024181912_AdjustInviteLength.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20221228033214_FileCacheSize.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20221228033214_FileCacheSize.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20230111092127_IsBannedForAuth.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20230111092127_IsBannedForAuth.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20230118184347_FilesUploadDate.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20230118184347_FilesUploadDate.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20230126163758_GroupPerms.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20230126163758_GroupPerms.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20230131193425_AddPrimaryUserToAuth.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20230131193425_AddPrimaryUserToAuth.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20230228001033_UserPerms.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20230228001033_UserPerms.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20230319015307_UserProfileData.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20230319015307_UserProfileData.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20230319114005_UserProfileReports.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20230319114005_UserProfileReports.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20230420075153_DisableVFX.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20230420075153_DisableVFX.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20250627174017_Bump.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20250627174017_Bump.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20250627174541_MCDO.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20250627174541_MCDO.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20250627204223_AllowedGroup.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20250627204223_AllowedGroup.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/MareDbContextModelSnapshot.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Models/Auth.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Models/Banned.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Models/BannedRegistrations.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Models/CharaData.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Models/ClientPair.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Models/FileCache.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Models/ForbiddenUploadEntry.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Models/Group.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Models/GroupBan.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Models/GroupPair.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Models/GroupTempInvite.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Models/LodeStoneAuth.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Models/User.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Models/UserProfileData.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Models/UserProfileDataReport.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/RequirementHandlers/UserRequirement.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/RequirementHandlers/UserRequirementHandler.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/RequirementHandlers/UserRequirements.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Services/IConfigurationService.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationController.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationServiceClient.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationServiceServer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Utils/AllowedControllersFeatureProvider.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Utils/ClientMessage.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Utils/Configuration/AuthServiceConfiguration.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Utils/Configuration/CdnShardConfiguration.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Utils/Configuration/IMareConfiguration.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Utils/Configuration/MareConfigurationBase.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Utils/Configuration/ServerConfiguration.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Utils/Configuration/ServicesConfiguration.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Utils/Configuration/StaticFilesServerConfiguration.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Utils/IdBasedUserIdProvider.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Utils/MareClaimTypes.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Utils/RemoteConfigurationAttribute.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Utils/ServerTokenGenerator.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Utils/SharedDbFunctions.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Utils/StringUtils.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/.config/dotnet-tools.json create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/CacheController.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/ControllerBase.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/DistributionController.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/MainController.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/RequestController.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/ServerFilesController.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/DummyHub.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/MareSynchronosStaticFilesServer.csproj create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Program.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Properties/launchSettings.json create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Services/CachedFileProvider.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Services/ColdTouchHashService.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Services/FileCleanupService.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Services/FilePreFetchService.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Services/FileStatisticsService.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Services/IClientReadyMessageService.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Services/ITouchHashService.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Services/MainClientReadyMessageService.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Services/RequestQueueService.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Services/ShardClientReadyMessageService.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Services/ShardTouchMessageService.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Startup.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/CountedStream.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/FilePathUtil.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/HashingStream.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/RequestBlockFileListResult.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/RequestBlockFileListResultFactory.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/TeeStream.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/UserQueueEntry.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/UserRequest.cs create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/appsettings.Development.json create mode 100644 MareSynchronosServer/MareSynchronosStaticFilesServer/appsettings.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..084da0b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,284 @@ +# Created by https://www.gitignore.io/api/csharp + +### Csharp ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ +Properties/launchSettings.json + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/ +tools/Cake.CoreCLR +.vscode +tools +.dotnet +Dockerfile + +# .env file contains default environment variables for docker +.env +.git/ \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae8dc13 --- /dev/null +++ b/.gitignore @@ -0,0 +1,357 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# docker run data +Docker/run/data/ + +**/obj/ +**/bin/ +**/.idea/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..8706603 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "MareAPI"] + path = MareAPI + url = https://github.com/Eauldane/SnowcloakAPI.git diff --git a/Docker/Readme.md b/Docker/Readme.md new file mode 100644 index 0000000..99b3600 --- /dev/null +++ b/Docker/Readme.md @@ -0,0 +1,39 @@ +# Mare Synchronos Docker Setup +This is primarily aimed at developers who want to spin up their own local server for development purposes without having to spin up a VM. +Obligatory requires Docker to be installed on the machine. + +There are two directories: `build` and `run` + +## 1. build images +There is two ways to build the necessary docker images which are differentiated by the folders `-local` and `-git` +- -local will run the image build against the current locally present sources +- -git will run the image build against the latest git main commit +It is possible to build all required images at once by running `docker-build.bat/sh` (Server, Servies, StaticFilesServer) or all 3 separately with `docker-build-.bat/sh` + +## 2. Configure ports + token +You should set up 2 environment variables that hold server specific configuration and open up ports. +The default ports used through the provided configuration are `6000` for the main server and `6200` as well as `6201` for the files downloads. +Both ports should be open to your computer through your router if you wish to test this with clients. + +Furthermore there are two environment variables `DEV_MARE_CDNURL` and `DEV_MARE_DISCORDTOKEN` which you are required to set. +`DEV_MARE_CDNURL` should point to `http://:6200/cache/` and `DEV_MARE_DISCORDTOKEN` is an oauth token from a bot you need to create through the Discord bot portal. +You should also set `DEV_MARE_CDNURL2` to `http://:6201/cache/` +It is enough to set them as User variables. The compose files refer to those environment variables to overwrite configuration settings for the Server and Services to set those respective values. +It is also possible to set those values in the configuration.json files themselves. +Without a valid Discord bot you will not be able to register accounts without fumbling around in the PostgreSQL database. + +## 3. Run Mare Server +The run folder contains two major Mare configurations which is `standalone` and `sharded`. +Both configurations default to port `6000` for the main server connection and `6200` for the files downloads. Sharded configuration additionally uses `6201` for downloads. No HTTPS. +All `appsettings.json` configurations provided are extensive at the point of writing, note the differences between the shard configurations and the main servers respectively. +They can be used as examples if you want to spin up your own servers otherwise. + +The scripts to start the respective services are divided by name, the `daemon-start/stop` files use `compose up -d` to run it in the background and to be able to stop the containers as well. +The respective docker-compose files lie in the `compose` folder. I would not recommend editing them unless you know what you are doing. +All data (postgresql and files uploads) will be thrown into the `data` folder after startup. +All logs from the mare services will be thrown into `logs`, divided by shard, where applicable. + +The `standalone` configuration features PostgeSQL, Mare Server, Mare StaticFilesServer and Mare Services. +The `sharded` configuration features PostgreSQL, Redis, HAProxy, Mare Server Main, 2 Mare Server Shards, Mare Services, Mare StaticFilesServer Main and 2 Mare StaticFilesServer Shards. +Haproxy is set up that it takes the same ports as the `standalone` configuration and distributes the connections between the shards. +In theory it should be possible to switch between the `standalone` and `sharded` configuration by shutting down one composition container and starting up the other. They share the same Database. \ No newline at end of file diff --git a/Docker/build/Dockerfile-MareSynchronosAuthService b/Docker/build/Dockerfile-MareSynchronosAuthService new file mode 100644 index 0000000..9ede4d0 --- /dev/null +++ b/Docker/build/Dockerfile-MareSynchronosAuthService @@ -0,0 +1,32 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 as BUILD + +COPY MareAPI /server/MareAPI +COPY MareSynchronosServer/MareSynchronosShared /server/MareSynchronosServer/MareSynchronosShared +COPY MareSynchronosServer/MareSynchronosAuthService /server/MareSynchronosServer/MareSynchronosAuthService + +WORKDIR /server/MareSynchronosServer/MareSynchronosAuthService/ + +RUN dotnet publish \ + --configuration=Debug \ + --os=linux \ + --output=/build \ + MareSynchronosAuthService.csproj + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 + +RUN adduser \ + --disabled-password \ + --group \ + --no-create-home \ + --quiet \ + --system \ + mare + +COPY --from=BUILD /build /opt/MareSynchronosAuthService +RUN chown -R mare:mare /opt/MareSynchronosAuthService +RUN apt-get update; apt-get install curl -y + +USER mare:mare +WORKDIR /opt/MareSynchronosAuthService + +CMD ["./MareSynchronosAuthService"] diff --git a/Docker/build/Dockerfile-MareSynchronosAuthService-git b/Docker/build/Dockerfile-MareSynchronosAuthService-git new file mode 100644 index 0000000..de9b7c1 --- /dev/null +++ b/Docker/build/Dockerfile-MareSynchronosAuthService-git @@ -0,0 +1,30 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 as BUILD + +RUN git clone --recurse-submodules https://github.com/Penumbra-Sync/server + +WORKDIR /server/MareSynchronosServer/MareSynchronosAuthService/ + +RUN dotnet publish \ + --configuration=Release \ + --os=linux \ + --output=/MareSynchronosAuthService \ + MareSynchronosAuthService.csproj + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 + +RUN adduser \ + --disabled-password \ + --group \ + --no-create-home \ + --quiet \ + --system \ + mare + +COPY --from=BUILD /MareSynchronosAuthService /opt/MareSynchronosAuthService +RUN chown -R mare:mare /opt/MareSynchronosAuthService +RUN apt-get update; apt-get install curl -y + +USER mare:mare +WORKDIR /opt/MareSynchronosAuthService + +CMD ["./MareSynchronosAuthService"] diff --git a/Docker/build/Dockerfile-MareSynchronosServer b/Docker/build/Dockerfile-MareSynchronosServer new file mode 100644 index 0000000..ea802c4 --- /dev/null +++ b/Docker/build/Dockerfile-MareSynchronosServer @@ -0,0 +1,32 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 as BUILD + +COPY MareAPI /server/MareAPI +COPY MareSynchronosServer/MareSynchronosShared /server/MareSynchronosServer/MareSynchronosShared +COPY MareSynchronosServer/MareSynchronosServer /server/MareSynchronosServer/MareSynchronosServer + +WORKDIR /server/MareSynchronosServer/MareSynchronosServer/ + +RUN dotnet publish \ + --configuration=Release \ + --os=linux \ + --output=/build \ + MareSynchronosServer.csproj + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 + +RUN adduser \ + --disabled-password \ + --group \ + --no-create-home \ + --quiet \ + --system \ + mare + +COPY --from=BUILD /build /opt/MareSynchronosServer +RUN chown -R mare:mare /opt/MareSynchronosServer +RUN apt-get update; apt-get install curl -y + +USER mare:mare +WORKDIR /opt/MareSynchronosServer + +CMD ["./MareSynchronosServer"] diff --git a/Docker/build/Dockerfile-MareSynchronosServer-git b/Docker/build/Dockerfile-MareSynchronosServer-git new file mode 100644 index 0000000..fec5c0b --- /dev/null +++ b/Docker/build/Dockerfile-MareSynchronosServer-git @@ -0,0 +1,30 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 as BUILD + +RUN git clone --recurse-submodules https://github.com/Penumbra-Sync/server + +WORKDIR /server/MareSynchronosServer/MareSynchronosServer/ + +RUN dotnet publish \ + --configuration=Release \ + --os=linux \ + --output=/MareSynchronosServer \ + MareSynchronosServer.csproj + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 + +RUN adduser \ + --disabled-password \ + --group \ + --no-create-home \ + --quiet \ + --system \ + mare + +COPY --from=BUILD /MareSynchronosServer /opt/MareSynchronosServer +RUN chown -R mare:mare /opt/MareSynchronosServer +RUN apt-get update; apt-get install curl -y + +USER mare:mare +WORKDIR /opt/MareSynchronosServer + +CMD ["./MareSynchronosServer"] diff --git a/Docker/build/Dockerfile-MareSynchronosServices b/Docker/build/Dockerfile-MareSynchronosServices new file mode 100644 index 0000000..eeba345 --- /dev/null +++ b/Docker/build/Dockerfile-MareSynchronosServices @@ -0,0 +1,32 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 as BUILD + +COPY MareAPI /server/MareAPI +COPY MareSynchronosServer/MareSynchronosShared /server/MareSynchronosServer/MareSynchronosShared +COPY MareSynchronosServer/MareSynchronosServices /server/MareSynchronosServer/MareSynchronosServices + +WORKDIR /server/MareSynchronosServer/MareSynchronosServices/ + +RUN dotnet publish \ + --configuration=Release \ + --os=linux \ + --output=/build \ + MareSynchronosServices.csproj + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 + +RUN adduser \ + --disabled-password \ + --group \ + --no-create-home \ + --quiet \ + --system \ + mare + +COPY --from=BUILD /build /opt/MareSynchronosServices +RUN chown -R mare:mare /opt/MareSynchronosServices +RUN apt-get update; apt-get install curl -y + +USER mare:mare +WORKDIR /opt/MareSynchronosServices + +CMD ["./MareSynchronosServices"] diff --git a/Docker/build/Dockerfile-MareSynchronosServices-git b/Docker/build/Dockerfile-MareSynchronosServices-git new file mode 100644 index 0000000..964d0ac --- /dev/null +++ b/Docker/build/Dockerfile-MareSynchronosServices-git @@ -0,0 +1,30 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 as BUILD + +RUN git clone --recurse-submodules https://github.com/Penumbra-Sync/server + +WORKDIR /server/MareSynchronosServer/MareSynchronosServices/ + +RUN dotnet publish \ + --configuration=Release \ + --os=linux \ + --output=/MareSynchronosServices \ + MareSynchronosServices.csproj + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 + +RUN adduser \ + --disabled-password \ + --group \ + --no-create-home \ + --quiet \ + --system \ + mare + +COPY --from=BUILD /MareSynchronosServices /opt/MareSynchronosServices +RUN chown -R mare:mare /opt/MareSynchronosServices +RUN apt-get update; apt-get install curl -y + +USER mare:mare +WORKDIR /opt/MareSynchronosServices + +CMD ["./MareSynchronosServices"] diff --git a/Docker/build/Dockerfile-MareSynchronosStaticFilesServer b/Docker/build/Dockerfile-MareSynchronosStaticFilesServer new file mode 100644 index 0000000..ff2adb2 --- /dev/null +++ b/Docker/build/Dockerfile-MareSynchronosStaticFilesServer @@ -0,0 +1,32 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 as BUILD + +COPY MareAPI /server/MareAPI +COPY MareSynchronosServer/MareSynchronosShared /server/MareSynchronosServer/MareSynchronosShared +COPY MareSynchronosServer/MareSynchronosStaticFilesServer /server/MareSynchronosServer/MareSynchronosStaticFilesServer + +WORKDIR /server/MareSynchronosServer/MareSynchronosStaticFilesServer/ + +RUN dotnet publish \ + --configuration=Release \ + --os=linux \ + --output=/build \ + MareSynchronosStaticFilesServer.csproj + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 + +RUN adduser \ + --disabled-password \ + --group \ + --no-create-home \ + --quiet \ + --system \ + mare + +COPY --from=BUILD /build /opt/MareSynchronosStaticFilesServer +RUN chown -R mare:mare /opt/MareSynchronosStaticFilesServer +RUN apt-get update; apt-get install curl -y + +USER mare:mare +WORKDIR /opt/MareSynchronosStaticFilesServer + +CMD ["./MareSynchronosStaticFilesServer"] diff --git a/Docker/build/Dockerfile-MareSynchronosStaticFilesServer-git b/Docker/build/Dockerfile-MareSynchronosStaticFilesServer-git new file mode 100644 index 0000000..579c38b --- /dev/null +++ b/Docker/build/Dockerfile-MareSynchronosStaticFilesServer-git @@ -0,0 +1,30 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 as BUILD + +RUN git clone --recurse-submodules https://github.com/Penumbra-Sync/server + +WORKDIR /server/MareSynchronosServer/MareSynchronosStaticFilesServer/ + +RUN dotnet publish \ + --configuration=Release \ + --os=linux \ + --output=/MareSynchronosStaticFilesServer \ + MareSynchronosStaticFilesServer.csproj + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 + +RUN adduser \ + --disabled-password \ + --group \ + --no-create-home \ + --quiet \ + --system \ + mare + +COPY --from=BUILD /MareSynchronosStaticFilesServer /opt/MareSynchronosStaticFilesServer +RUN chown -R mare:mare /opt/MareSynchronosStaticFilesServer +RUN apt-get update; apt-get install curl -y + +USER mare:mare +WORKDIR /opt/MareSynchronosStaticFilesServer + +CMD ["./MareSynchronosStaticFilesServer"] diff --git a/Docker/build/linux-git/docker-build-authservice.sh b/Docker/build/linux-git/docker-build-authservice.sh new file mode 100644 index 0000000..fd7d396 --- /dev/null +++ b/Docker/build/linux-git/docker-build-authservice.sh @@ -0,0 +1,2 @@ +#!/bin/sh +docker build -t darkarchon/mare-synchronos-authservice:latest . -f ../Dockerfile-MareSynchronosAuthService-git --no-cache --pull --force-rm \ No newline at end of file diff --git a/Docker/build/linux-git/docker-build-server.sh b/Docker/build/linux-git/docker-build-server.sh new file mode 100644 index 0000000..1815f92 --- /dev/null +++ b/Docker/build/linux-git/docker-build-server.sh @@ -0,0 +1,2 @@ +#!/bin/sh +docker build -t darkarchon/mare-synchronos-server:latest . -f ../Dockerfile-MareSynchronosServer-git --no-cache --pull --force-rm \ No newline at end of file diff --git a/Docker/build/linux-git/docker-build-services.sh b/Docker/build/linux-git/docker-build-services.sh new file mode 100644 index 0000000..93f9be7 --- /dev/null +++ b/Docker/build/linux-git/docker-build-services.sh @@ -0,0 +1,2 @@ +#!/bin/sh +docker build -t darkarchon/mare-synchronos-services:latest . -f ../Dockerfile-MareSynchronosServices-git --no-cache --pull --force-rm \ No newline at end of file diff --git a/Docker/build/linux-git/docker-build-staticfilesserver.sh b/Docker/build/linux-git/docker-build-staticfilesserver.sh new file mode 100644 index 0000000..3ec753c --- /dev/null +++ b/Docker/build/linux-git/docker-build-staticfilesserver.sh @@ -0,0 +1,2 @@ +#!/bin/sh +docker build -t darkarchon/mare-synchronos-staticfilesserver:latest . -f ../Dockerfile-MareSynchronosStaticFilesServer-git --no-cache --pull --force-rm \ No newline at end of file diff --git a/Docker/build/linux-git/docker-build.sh b/Docker/build/linux-git/docker-build.sh new file mode 100644 index 0000000..958ab02 --- /dev/null +++ b/Docker/build/linux-git/docker-build.sh @@ -0,0 +1,5 @@ +#!/bin/sh +./docker-build-server.sh +./docker-build-authservice.sh +./docker-build-services.sh +./docker-build-staticfilesserver.sh \ No newline at end of file diff --git a/Docker/build/linux-local/docker-build-authservice.sh b/Docker/build/linux-local/docker-build-authservice.sh new file mode 100644 index 0000000..87859cd --- /dev/null +++ b/Docker/build/linux-local/docker-build-authservice.sh @@ -0,0 +1,2 @@ +#!/bin/sh +docker build -t darkarchon/mare-synchronos-authservice:latest . -f ../Dockerfile-MareSynchronosAuthService --no-cache --pull --force-rm \ No newline at end of file diff --git a/Docker/build/linux-local/docker-build-server.sh b/Docker/build/linux-local/docker-build-server.sh new file mode 100644 index 0000000..4c511aa --- /dev/null +++ b/Docker/build/linux-local/docker-build-server.sh @@ -0,0 +1,4 @@ +#!/bin/sh +cd ../../../ +docker build -t darkarchon/mare-synchronos-server:latest . -f ../Dockerfile-MareSynchronosServer --no-cache --pull --force-rm +cd Docker/build/linux-local \ No newline at end of file diff --git a/Docker/build/linux-local/docker-build-services.sh b/Docker/build/linux-local/docker-build-services.sh new file mode 100644 index 0000000..f5a7f5f --- /dev/null +++ b/Docker/build/linux-local/docker-build-services.sh @@ -0,0 +1,4 @@ +#!/bin/sh +cd ../../../ +docker build -t darkarchon/mare-synchronos-services:latest . -f ../Dockerfile-MareSynchronosServices --no-cache --pull --force-rm +cd Docker/build/linux-local \ No newline at end of file diff --git a/Docker/build/linux-local/docker-build-staticfilesserver.sh b/Docker/build/linux-local/docker-build-staticfilesserver.sh new file mode 100644 index 0000000..3881a83 --- /dev/null +++ b/Docker/build/linux-local/docker-build-staticfilesserver.sh @@ -0,0 +1,4 @@ +#!/bin/sh +cd ../../../ +docker build -t darkarchon/mare-synchronos-staticfilesserver:latest . -f ../Dockerfile-MareSynchronosStaticFilesServer --no-cache --pull --force-rm +cd Docker/build/linux-local \ No newline at end of file diff --git a/Docker/build/linux-local/docker-build.sh b/Docker/build/linux-local/docker-build.sh new file mode 100644 index 0000000..958ab02 --- /dev/null +++ b/Docker/build/linux-local/docker-build.sh @@ -0,0 +1,5 @@ +#!/bin/sh +./docker-build-server.sh +./docker-build-authservice.sh +./docker-build-services.sh +./docker-build-staticfilesserver.sh \ No newline at end of file diff --git a/Docker/build/windows-git/docker-build-authservice.bat b/Docker/build/windows-git/docker-build-authservice.bat new file mode 100644 index 0000000..e504c93 --- /dev/null +++ b/Docker/build/windows-git/docker-build-authservice.bat @@ -0,0 +1,3 @@ +@echo off + +docker build -t darkarchon/mare-synchronos-authservice:latest . -f ..\Dockerfile-MareSynchronosAuthService-git --no-cache --pull --force-rm \ No newline at end of file diff --git a/Docker/build/windows-git/docker-build-server.bat b/Docker/build/windows-git/docker-build-server.bat new file mode 100644 index 0000000..7745a71 --- /dev/null +++ b/Docker/build/windows-git/docker-build-server.bat @@ -0,0 +1,2 @@ +@echo off +docker build -t darkarchon/mare-synchronos-server:latest . -f ..\Dockerfile-MareSynchronosServer-git --no-cache --pull --force-rm \ No newline at end of file diff --git a/Docker/build/windows-git/docker-build-services.bat b/Docker/build/windows-git/docker-build-services.bat new file mode 100644 index 0000000..c99c8d5 --- /dev/null +++ b/Docker/build/windows-git/docker-build-services.bat @@ -0,0 +1,3 @@ +@echo off + +docker build -t darkarchon/mare-synchronos-services:latest . -f ..\Dockerfile-MareSynchronosServices-git --no-cache --pull --force-rm \ No newline at end of file diff --git a/Docker/build/windows-git/docker-build-staticfilesserver.bat b/Docker/build/windows-git/docker-build-staticfilesserver.bat new file mode 100644 index 0000000..ca314e0 --- /dev/null +++ b/Docker/build/windows-git/docker-build-staticfilesserver.bat @@ -0,0 +1,3 @@ +@echo off + +docker build -t darkarchon/mare-synchronos-staticfilesserver:latest . -f ..\Dockerfile-MareSynchronosStaticFilesServer-git --no-cache --pull --force-rm \ No newline at end of file diff --git a/Docker/build/windows-git/docker-build.bat b/Docker/build/windows-git/docker-build.bat new file mode 100644 index 0000000..2deba89 --- /dev/null +++ b/Docker/build/windows-git/docker-build.bat @@ -0,0 +1,6 @@ +@echo off + +call docker-build-server.bat +call docker-build-authservice.bat +call docker-build-services.bat +call docker-build-staticfilesserver.bat \ No newline at end of file diff --git a/Docker/build/windows-local/docker-build-authservice.bat b/Docker/build/windows-local/docker-build-authservice.bat new file mode 100644 index 0000000..335a328 --- /dev/null +++ b/Docker/build/windows-local/docker-build-authservice.bat @@ -0,0 +1,4 @@ +@echo off +cd ..\..\..\ +docker build -t darkarchon/mare-synchronos-authservice:latest . -f Docker\build\Dockerfile-MareSynchronosAuthService --no-cache --pull --force-rm +cd Docker\build\windows-local \ No newline at end of file diff --git a/Docker/build/windows-local/docker-build-server.bat b/Docker/build/windows-local/docker-build-server.bat new file mode 100644 index 0000000..eae4059 --- /dev/null +++ b/Docker/build/windows-local/docker-build-server.bat @@ -0,0 +1,4 @@ +@echo off +cd ..\..\..\ +docker build -t darkarchon/mare-synchronos-server:latest . -f Docker\build\Dockerfile-MareSynchronosServer --no-cache --pull --force-rm +cd Docker\build\windows-local \ No newline at end of file diff --git a/Docker/build/windows-local/docker-build-services.bat b/Docker/build/windows-local/docker-build-services.bat new file mode 100644 index 0000000..aff8e21 --- /dev/null +++ b/Docker/build/windows-local/docker-build-services.bat @@ -0,0 +1,4 @@ +@echo off +cd ..\..\..\ +docker build -t darkarchon/mare-synchronos-services:latest . -f Docker\build\Dockerfile-MareSynchronosServices --no-cache --pull --force-rm +cd Docker\build\windows-local \ No newline at end of file diff --git a/Docker/build/windows-local/docker-build-staticfilesserver.bat b/Docker/build/windows-local/docker-build-staticfilesserver.bat new file mode 100644 index 0000000..72b174f --- /dev/null +++ b/Docker/build/windows-local/docker-build-staticfilesserver.bat @@ -0,0 +1,4 @@ +@echo off +cd ..\..\..\ +docker build -t darkarchon/mare-synchronos-staticfilesserver:latest . -f Docker\build\Dockerfile-MareSynchronosStaticFilesServer --no-cache --pull --force-rm +cd Docker\build\windows-local \ No newline at end of file diff --git a/Docker/build/windows-local/docker-build.bat b/Docker/build/windows-local/docker-build.bat new file mode 100644 index 0000000..2deba89 --- /dev/null +++ b/Docker/build/windows-local/docker-build.bat @@ -0,0 +1,6 @@ +@echo off + +call docker-build-server.bat +call docker-build-authservice.bat +call docker-build-services.bat +call docker-build-staticfilesserver.bat \ No newline at end of file diff --git a/Docker/run/compose/mare-sharded.yml b/Docker/run/compose/mare-sharded.yml new file mode 100644 index 0000000..a683216 --- /dev/null +++ b/Docker/run/compose/mare-sharded.yml @@ -0,0 +1,145 @@ +services: + postgres: + image: postgres:latest + restart: always + environment: + POSTGRES_DB: mare + POSTGRES_USER: mare + POSTGRES_PASSWORD: secretdevpassword + volumes: + - ../data/postgresql/:/var/lib/postgresql/data + - postgres_socket:/var/run/postgresql:rw + healthcheck: + test: ["CMD-SHELL", "pg_isready -U mare"] + interval: 5s + start_period: 5s + timeout: 5s + retries: 5 + + haproxy: + image: haproxy:latest + restart: always + ports: + - 6000:6000/tcp + volumes: + - ../config/sharded/haproxy-shards.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro + depends_on: + mare-server: + condition: service_healthy + + redis: + image: redis:latest + command: [sh, -c, "rm -f /data/dump.rdb && redis-server --save \"\" --appendonly no --requirepass secretredispassword"] + volumes: + - cache:/data + + mare-server: + image: darkarchon/mare-synchronos-server:latest + restart: on-failure + environment: + MareSynchronos__CdnFullUrl: "${DEV_MARE_CDNURL}" + volumes: + - ../config/sharded/server-shard-main.json:/opt/MareSynchronosServer/appsettings.json + - ../log/server-shard-main/:/opt/MareSynchronosServer/logs/:rw + - postgres_socket:/var/run/postgresql/:rw + depends_on: + postgres: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl --fail http://localhost:6000/health || exit 1"] + retries: 60 + start_period: 10s + timeout: 1s + + mare-shard-1: + image: darkarchon/mare-synchronos-server:latest + restart: on-failure + volumes: + - ../config/sharded/server-shard-1.json:/opt/MareSynchronosServer/appsettings.json + - ../log/server-shard-1/:/opt/MareSynchronosServer/logs/:rw + - postgres_socket:/var/run/postgresql/:rw + depends_on: + mare-server: + condition: service_healthy + + mare-shard-2: + image: darkarchon/mare-synchronos-server:latest + restart: on-failure + volumes: + - ../config/sharded/server-shard-2.json:/opt/MareSynchronosServer/appsettings.json + - ../log/server-shard-2/:/opt/MareSynchronosServer/logs/:rw + - postgres_socket:/var/run/postgresql/:rw + depends_on: + mare-server: + condition: service_healthy + + mare-services: + image: darkarchon/mare-synchronos-services:latest + restart: on-failure + environment: + MareSynchronos__DiscordBotToken: "${DEV_MARE_DISCORDTOKEN}" + MareSynchronos__DiscordChannelForMessages: "${DEV_MARE_DISCORDCHANNEL}" + volumes: + - ../config/standalone/services-standalone.json:/opt/MareSynchronosServices/appsettings.json + - ../log/services-standalone/:/opt/MareSynchronosServices/logs/:rw + - postgres_socket:/var/run/postgresql/:rw + depends_on: + mare-server: + condition: service_healthy + + mare-files: + image: darkarchon/mare-synchronos-staticfilesserver:latest + restart: on-failure + ports: + - 6200:6200/tcp + environment: + MareSynchronos__CdnShardConfiguration__0__CdnFullUrl: "${DEV_MARE_FILES1}" + MareSynchronos__CdnShardConfiguration__0__FileMatch: "^[012345678]" + MareSynchronos__CdnShardConfiguration__1__CdnFullUrl: "${DEV_MARE_FILES2}" + MareSynchronos__CdnShardConfiguration__1__FileMatch: "^[789ABCDEF]" + volumes: + - ../config/sharded/files-shard-main.json:/opt/MareSynchronosStaticFilesServer/appsettings.json + - ../log/files-standalone/:/opt/MareSynchronosStaticFilesServer/logs/:rw + - postgres_socket:/var/run/postgresql/:rw + - ../data/files-shard-main/:/marecache/:rw + depends_on: + mare-server: + condition: service_healthy + healthcheck: + test: curl --fail http://localhost:6200/health || exit 1 + retries: 60 + start_period: 10s + timeout: 1s + + mare-files-shard-1: + image: darkarchon/mare-synchronos-staticfilesserver:latest + restart: on-failure + volumes: + - ../config/sharded/files-shard-1.json:/opt/MareSynchronosStaticFilesServer/appsettings.json + - ../log/files-shard-1/:/opt/MareSynchronosStaticFilesServer/logs/:rw + - postgres_socket:/var/run/postgresql/:rw + - ../data/files-shard-1/:/marecache/:rw + ports: + - 6201:6200/tcp + depends_on: + mare-files: + condition: service_healthy + + mare-files-shard-2: + image: darkarchon/mare-synchronos-staticfilesserver:latest + restart: on-failure + volumes: + - ../config/sharded/files-shard-2.json:/opt/MareSynchronosStaticFilesServer/appsettings.json + - ../log/files-shard-2/:/opt/MareSynchronosStaticFilesServer/logs/:rw + - postgres_socket:/var/run/postgresql/:rw + - ../data/files-shard-2/:/marecache/:rw + ports: + - 6202:6200/tcp + depends_on: + mare-files: + condition: service_healthy + +volumes: + cache: + driver: local + postgres_socket: \ No newline at end of file diff --git a/Docker/run/compose/mare-standalone.yml b/Docker/run/compose/mare-standalone.yml new file mode 100644 index 0000000..bd114af --- /dev/null +++ b/Docker/run/compose/mare-standalone.yml @@ -0,0 +1,102 @@ +services: + postgres: + image: postgres:latest + restart: always + ports: + - 5432:5432/tcp + environment: + POSTGRES_DB: mare + POSTGRES_USER: mare + POSTGRES_PASSWORD: secretdevpassword + volumes: + - ../data/postgresql/:/var/lib/postgresql/data + - postgres_socket:/var/run/postgresql:rw + healthcheck: + test: ["CMD-SHELL", "pg_isready -U mare"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: redis:latest + command: [sh, -c, "rm -f /data/dump.rdb && redis-server --save \"\" --appendonly no --requirepass secretredispassword"] + volumes: + - cache:/data + + mare-server: + image: darkarchon/mare-synchronos-server:latest + restart: on-failure + ports: + - 6000:6000/tcp + environment: + MareSynchronos__CdnFullUrl: "${DEV_MARE_CDNURL}" + DOTNET_USE_POLLING_FILE_WATCHER: 1 + volumes: + - ../config/standalone/server-standalone.json:/opt/MareSynchronosServer/appsettings.json + - ../log/server-standalone/:/opt/MareSynchronosServer/logs/:rw + - postgres_socket:/var/run/postgresql/:rw + depends_on: + postgres: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl --fail http://localhost:6000/health || exit 1"] + retries: 60 + start_period: 10s + timeout: 1s + + mare-auth: + image: darkarchon/mare-synchronos-authservice:latest + restart: on-failure + environment: + DOTNET_USE_POLLING_FILE_WATCHER: 1 + volumes: + - ../config/standalone/authservice-standalone.json:/opt/MareSynchronosAuthService/appsettings.json + - ../log/authservice-standalone/:/opt/MareSynchronosAuthService/logs/:rw + - postgres_socket:/var/run/postgresql/:rw + depends_on: + mare-server: + condition: service_healthy + postgres: + condition: service_healthy + + mare-services: + image: darkarchon/mare-synchronos-services:latest + restart: on-failure + environment: + MareSynchronos__DiscordBotToken: "${DEV_MARE_DISCORDTOKEN}" + MareSynchronos__DiscordChannelForMessages: "${DEV_MARE_DISCORDCHANNEL}" + MareSynchronos__DiscordChannelForReports: "${DEV_MARE_DISCORDCHANNEL}" + DOTNET_USE_POLLING_FILE_WATCHER: 1 + volumes: + - ../config/standalone/services-standalone.json:/opt/MareSynchronosServices/appsettings.json + - ../log/services-standalone/:/opt/MareSynchronosServices/logs/:rw + - postgres_socket:/var/run/postgresql/:rw + depends_on: + mare-server: + condition: service_healthy + postgres: + condition: service_healthy + + mare-files: + image: darkarchon/mare-synchronos-staticfilesserver:latest + ports: + - 6200:6200/tcp + restart: on-failure + environment: + MareSynchronos__CdnFullUrl: "${DEV_MARE_CDNURL}" + DOTNET_USE_POLLING_FILE_WATCHER: 1 + volumes: + - ../config/standalone/files-standalone.json:/opt/MareSynchronosStaticFilesServer/appsettings.json + - ../log/files-standalone/:/opt/MareSynchronosStaticFilesServer/logs/:rw + - postgres_socket:/var/run/postgresql/:rw + - ../data/files-standalone/:/marecache/:rw + depends_on: + postgres: + condition: service_healthy + mare-server: + condition: service_healthy + +volumes: + postgres_socket: + cache: + driver: local \ No newline at end of file diff --git a/Docker/run/config/sharded/files-shard-1.json b/Docker/run/config/sharded/files-shard-1.json new file mode 100644 index 0000000..2b36db7 --- /dev/null +++ b/Docker/run/config/sharded/files-shard-1.json @@ -0,0 +1,53 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=/var/run/postgresql;Port=5432;Database=mare;Username=mare;Keepalive=15;Minimum Pool Size=10;Maximum Pool Size=50;No Reset On Close=true;Max Auto Prepare=50;Enlist=false" + }, + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "MareSynchronosStaticFilesServer": "Debug", + "MareSynchronosShared": "Information", + "System.IO": "Information" + }, + "File": { + "BasePath": "logs", + "FileAccessMode": "KeepOpenAndAutoFlush", + "FileEncodingName": "utf-8", + "DateFormat": "yyyMMdd", + "MaxFileSize": 104857600, + "Files": [ + { + "Path": "///mare--.log" + } + ] + } + }, + "MareSynchronos": { + "DbContextPoolSize": 512, + "ShardName": "Files Shard 1", + "MetricsPort": 6250, + "ForcedDeletionOfFilesAfterHours": 2, + "CacheSizeHardLimitInGiB": 5, + "UnusedFileRetentionPeriodInDays": 14, + "CacheDirectory": "/marecache/", + "DownloadTimeoutSeconds": 30, + "DownloadQueueSize": 50, + "DownloadQueueReleaseSeconds": 15, + "RedisConnectionString": "redis,password=secretredispassword", + "Jwt": "teststringteststringteststringteststringteststringteststringteststringteststringteststringteststring", + "MainServerAddress": "http://mare-server:6000", + "MainFileServerAddress": "http://mare-files:6200" + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://+:6200" + } + } + }, + "IpRateLimiting": {}, + "IPRateLimitPolicies": {} +} \ No newline at end of file diff --git a/Docker/run/config/sharded/files-shard-2.json b/Docker/run/config/sharded/files-shard-2.json new file mode 100644 index 0000000..50a9307 --- /dev/null +++ b/Docker/run/config/sharded/files-shard-2.json @@ -0,0 +1,53 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=/var/run/postgresql;Port=5432;Database=mare;Username=mare;Keepalive=15;Minimum Pool Size=10;Maximum Pool Size=50;No Reset On Close=true;Max Auto Prepare=50;Enlist=false" + }, + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "MareSynchronosStaticFilesServer": "Debug", + "MareSynchronosShared": "Information", + "System.IO": "Information" + }, + "File": { + "BasePath": "logs", + "FileAccessMode": "KeepOpenAndAutoFlush", + "FileEncodingName": "utf-8", + "DateFormat": "yyyMMdd", + "MaxFileSize": 104857600, + "Files": [ + { + "Path": "///mare--.log" + } + ] + } + }, + "MareSynchronos": { + "DbContextPoolSize": 512, + "ShardName": "Files Shard 2", + "MetricsPort": 6250, + "ForcedDeletionOfFilesAfterHours": 2, + "CacheSizeHardLimitInGiB": 5, + "UnusedFileRetentionPeriodInDays": 14, + "CacheDirectory": "/marecache/", + "DownloadTimeoutSeconds": 30, + "DownloadQueueSize": 50, + "DownloadQueueReleaseSeconds": 15, + "RedisConnectionString": "redis,password=secretredispassword", + "Jwt": "teststringteststringteststringteststringteststringteststringteststringteststringteststringteststring", + "MainServerAddress": "http://mare-server:6000", + "MainFileServerAddress": "http://mare-files:6200" + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://+:6200" + } + } + }, + "IpRateLimiting": {}, + "IPRateLimitPolicies": {} +} \ No newline at end of file diff --git a/Docker/run/config/sharded/files-shard-main.json b/Docker/run/config/sharded/files-shard-main.json new file mode 100644 index 0000000..d481e10 --- /dev/null +++ b/Docker/run/config/sharded/files-shard-main.json @@ -0,0 +1,56 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=/var/run/postgresql;Port=5432;Database=mare;Username=mare;Keepalive=15;Minimum Pool Size=10;Maximum Pool Size=50;No Reset On Close=true;Max Auto Prepare=50;Enlist=false" + }, + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "MareSynchronosStaticFilesServer": "Information", + "MareSynchronosShared": "Information", + "System.IO": "Information" + }, + "File": { + "BasePath": "logs", + "FileAccessMode": "KeepOpenAndAutoFlush", + "FileEncodingName": "utf-8", + "DateFormat": "yyyMMdd", + "MaxFileSize": 104857600, + "Files": [ + { + "Path": "///mare--.log" + } + ] + } + }, + "MareSynchronos": { + "DbContextPoolSize": 512, + "ShardName": "Files", + "MetricsPort": 6250, + "FileServerGrpcAddress": "", + "ForcedDeletionOfFilesAfterHours": -1, + "CacheSizeHardLimitInGiB": -1, + "UnusedFileRetentionPeriodInDays": 14, + "CacheDirectory": "/marecache/", + "RemoteCacheSourceUri": "", + "RedisConnectionString": "redis,password=secretredispassword", + "Jwt": "teststringteststringteststringteststringteststringteststringteststringteststringteststringteststring", + "MainServerAddress": "http://mare-server:6000", + "MainFileServerAddress": "" + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://+:6200" + }, + "Grpc": { + "Protocols": "Http2", + "Url": "http://+:6205" + } + } + }, + "IpRateLimiting": {}, + "IPRateLimitPolicies": {} +} \ No newline at end of file diff --git a/Docker/run/config/sharded/haproxy-shards.cfg b/Docker/run/config/sharded/haproxy-shards.cfg new file mode 100644 index 0000000..2075e1a --- /dev/null +++ b/Docker/run/config/sharded/haproxy-shards.cfg @@ -0,0 +1,30 @@ +global + log /dev/log local0 + log /dev/log local1 notice + daemon + + ca-base /etc/ssl/certs + crt-base /etc/ssl/private + + ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 + ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 + ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets + +defaults + log global + mode http + option httplog + option dontlognull + timeout connect 5000 + timeout client 50000 + timeout server 50000 + +frontend mare + bind :6000 + default_backend mare-servers + +backend mare-servers + balance leastconn + cookie SERVER insert indirect nocache + server mare1 mare-shard-1:6000 cookie mare1 + server mare2 mare-shard-2:6000 cookie mare2 diff --git a/Docker/run/config/sharded/server-shard-1.json b/Docker/run/config/sharded/server-shard-1.json new file mode 100644 index 0000000..402babe --- /dev/null +++ b/Docker/run/config/sharded/server-shard-1.json @@ -0,0 +1,45 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=/var/run/postgresql;Port=5432;Database=mare;Username=mare;Keepalive=15;Minimum Pool Size=10;Maximum Pool Size=50;No Reset On Close=true;Max Auto Prepare=50;Enlist=false" + }, + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "MareSynchronosServer": "Information", + "MareSynchronosShared": "Information", + "System.IO": "Information" + }, + "File": { + "BasePath": "logs", + "FileAccessMode": "KeepOpenAndAutoFlush", + "FileEncodingName": "utf-8", + "DateFormat": "yyyMMdd", + "MaxFileSize": 104857600, + "Files": [ + { + "Path": "///mare--.log" + } + ] + } + }, + "MareSynchronos": { + "DbContextPoolSize": 512, + "ShardName": "Shard 1", + "MetricsPort": 6050, + "MainServerAddress": "http://mare-server:6000", + "RedisConnectionString": "redis,password=secretredispassword", + "Jwt": "teststringteststringteststringteststringteststringteststringteststringteststringteststringteststring" + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://+:6000" + } + } + }, + "IpRateLimiting": {}, + "IPRateLimitPolicies": {} +} \ No newline at end of file diff --git a/Docker/run/config/sharded/server-shard-2.json b/Docker/run/config/sharded/server-shard-2.json new file mode 100644 index 0000000..f8a3228 --- /dev/null +++ b/Docker/run/config/sharded/server-shard-2.json @@ -0,0 +1,45 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=/var/run/postgresql;Port=5432;Database=mare;Username=mare;Keepalive=15;Minimum Pool Size=10;Maximum Pool Size=50;No Reset On Close=true;Max Auto Prepare=50;Enlist=false" + }, + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "MareSynchronosServer": "Information", + "MareSynchronosShared": "Information", + "System.IO": "Information" + }, + "File": { + "BasePath": "logs", + "FileAccessMode": "KeepOpenAndAutoFlush", + "FileEncodingName": "utf-8", + "DateFormat": "yyyMMdd", + "MaxFileSize": 104857600, + "Files": [ + { + "Path": "///mare--.log" + } + ] + } + }, + "MareSynchronos": { + "DbContextPoolSize": 512, + "ShardName": "Shard 2", + "MetricsPort": 6050, + "MainServerAddress": "http://mare-server:6000", + "RedisConnectionString": "redis,password=secretredispassword", + "Jwt": "teststringteststringteststringteststringteststringteststringteststringteststringteststringteststring" + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://+:6000" + } + } + }, + "IpRateLimiting": {}, + "IPRateLimitPolicies": {} +} \ No newline at end of file diff --git a/Docker/run/config/sharded/server-shard-main.json b/Docker/run/config/sharded/server-shard-main.json new file mode 100644 index 0000000..5fb045d --- /dev/null +++ b/Docker/run/config/sharded/server-shard-main.json @@ -0,0 +1,71 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=/var/run/postgresql;Port=5432;Database=mare;Username=mare;Keepalive=15;Minimum Pool Size=10;Maximum Pool Size=50;No Reset On Close=true;Max Auto Prepare=50;Enlist=false" + }, + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "MareSynchronosServer": "Information", + "MareSynchronosShared": "Information", + "System.IO": "Information" + }, + "File": { + "BasePath": "logs", + "FileAccessMode": "KeepOpenAndAutoFlush", + "FileEncodingName": "utf-8", + "DateFormat": "yyyMMdd", + "MaxFileSize": 104857600, + "Files": [ + { + "Path": "///mare--.log" + } + ] + } + }, + "MareSynchronos": { + "DbContextPoolSize": 512, + "ShardName": "Main", + "MetricsPort": 6050, + "MainServerGrpcAddress": "", + "FailedAuthForTempBan": 5, + "TempBanDurationInMinutes": 5, + "Jwt": "teststringteststringteststringteststringteststringteststringteststringteststringteststringteststring", + "WhitelistedIps": [ + "" + ], + "RedisConnectionString": "redis,password=secretredispassword", + "CdnFullUrl": "http://localhost:6200/", + "StaticFileServiceAddress": "http://mare-files:6205", + "MaxExistingGroupsByUser": 3, + "MaxJoinedGroupsByUser": 6, + "MaxGroupUserCount": 100, + "PurgeUnusedAccounts": false, + "PurgeUnusedAccountsPeriodInDays": 14, + "CdnShardConfiguration": [ + { + "FileMatch": "^[01234567]", + "CdnFullUrl": "" + }, + { + "FileMatch": "^[89ABCDEF]", + "CdnFullUrl": "" + } + ] + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://+:6000" + }, + "Grpc": { + "Protocols": "Http2", + "Url": "http://+:6005" + } + } + }, + "IpRateLimiting": {}, + "IPRateLimitPolicies": {} +} \ No newline at end of file diff --git a/Docker/run/config/standalone/authservice-standalone.json b/Docker/run/config/standalone/authservice-standalone.json new file mode 100644 index 0000000..bc21fb2 --- /dev/null +++ b/Docker/run/config/standalone/authservice-standalone.json @@ -0,0 +1,42 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=/var/run/postgresql;Port=5432;Database=mare;Username=mare;Keepalive=15;Minimum Pool Size=10;Maximum Pool Size=50;No Reset On Close=true;Max Auto Prepare=50;Enlist=false" + }, + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "MareSynchronosServices": "Information", + "MareSynchronosShared": "Information", + "System.IO": "Information" + }, + "File": { + "BasePath": "logs", + "FileAccessMode": "KeepOpenAndAutoFlush", + "FileEncodingName": "utf-8", + "DateFormat": "yyyMMdd", + "MaxFileSize": 104857600, + "Files": [ + { + "Path": "///mare--.log" + } + ] + } + }, + "MareSynchronos": { + "DbContextPoolSize": 512, + "ShardName": "AuthServices", + "MetricsPort": 6150, + "Jwt": "teststringteststringteststringteststringteststringteststringteststringteststringteststringteststring", + "RedisConnectionString": "redis,password=secretredispassword", + "FailedAuthForTempBan": 5, + "UseGeoIP": false, + "GeoIPDbCityFile": "" + }, + "AllowedHosts": "*", + "Kestrel": { + }, + "IpRateLimiting": {}, + "IPRateLimitPolicies": {} +} \ No newline at end of file diff --git a/Docker/run/config/standalone/files-standalone.json b/Docker/run/config/standalone/files-standalone.json new file mode 100644 index 0000000..e87053c --- /dev/null +++ b/Docker/run/config/standalone/files-standalone.json @@ -0,0 +1,53 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=/var/run/postgresql;Port=5432;Database=mare;Username=mare;Keepalive=15;Minimum Pool Size=10;Maximum Pool Size=50;No Reset On Close=true;Max Auto Prepare=50;Enlist=false" + }, + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft": "Debug", + "Microsoft.Hosting.Lifetime": "Information", + "MareSynchronosStaticFilesServer": "Debug", + "MareSynchronosShared": "Debug", + "System.IO": "Information" + }, + "File": { + "BasePath": "logs", + "FileAccessMode": "KeepOpenAndAutoFlush", + "FileEncodingName": "utf-8", + "DateFormat": "yyyMMdd", + "MaxFileSize": 104857600, + "Files": [ + { + "Path": "///mare--.log" + } + ] + } + }, + "MareSynchronos": { + "DbContextPoolSize": 512, + "ShardName": "Files", + "MetricsPort": 6250, + "ForcedDeletionOfFilesAfterHours": -1, + "CacheSizeHardLimitInGiB": -1, + "UnusedFileRetentionPeriodInDays": 14, + "CacheDirectory": "/marecache/", + "RemoteCacheSourceUri": "", + "MainServerAddress": "http://mare-server:6000/", + "RedisConnectionString": "redis,password=secretredispassword", + "MainFileServerAddress": "", + "Jwt": "teststringteststringteststringteststringteststringteststringteststringteststringteststringteststring", + "UseColdStorage": false, + "IsDistributionNode": true + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://+:6200" + } + } + }, + "IpRateLimiting": {}, + "IPRateLimitPolicies": {} +} \ No newline at end of file diff --git a/Docker/run/config/standalone/server-standalone.json b/Docker/run/config/standalone/server-standalone.json new file mode 100644 index 0000000..84b76da --- /dev/null +++ b/Docker/run/config/standalone/server-standalone.json @@ -0,0 +1,57 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=/var/run/postgresql;Port=5432;Database=mare;Username=mare;Keepalive=15;Minimum Pool Size=10;Maximum Pool Size=50;No Reset On Close=true;Max Auto Prepare=50;Enlist=false" + }, + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "MareSynchronosServer": "Information", + "MareSynchronosShared": "Information", + "System.IO": "Information" + }, + "File": { + "BasePath": "logs", + "FileAccessMode": "KeepOpenAndAutoFlush", + "FileEncodingName": "utf-8", + "DateFormat": "yyyMMdd", + "MaxFileSize": 104857600, + "Files": [ + { + "Path": "///mare--.log" + } + ] + } + }, + "MareSynchronos": { + "DbContextPoolSize": 512, + "ShardName": "Main", + "MetricsPort": 6050, + "MainServerAddress": "", + "FailedAuthForTempBan": 5, + "TempBanDurationInMinutes": 5, + "Jwt": "teststringteststringteststringteststringteststringteststringteststringteststringteststringteststring", + "WhitelistedIps": [ + "" + ], + "RedisConnectionString": "redis,password=secretredispassword", + "CdnFullUrl": "http://localhost:6200", + "MaxExistingGroupsByUser": 3, + "MaxJoinedGroupsByUser": 6, + "MaxGroupUserCount": 100, + "PurgeUnusedAccounts": false, + "PurgeUnusedAccountsPeriodInDays": 14, + "ExpectedClientVersion": "0.8.0" + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://+:6000" + } + } + }, + "IpRateLimiting": {}, + "IPRateLimitPolicies": {} +} \ No newline at end of file diff --git a/Docker/run/config/standalone/services-standalone.json b/Docker/run/config/standalone/services-standalone.json new file mode 100644 index 0000000..56365a4 --- /dev/null +++ b/Docker/run/config/standalone/services-standalone.json @@ -0,0 +1,44 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=/var/run/postgresql;Port=5432;Database=mare;Username=mare;Keepalive=15;Minimum Pool Size=10;Maximum Pool Size=50;No Reset On Close=true;Max Auto Prepare=50;Enlist=false" + }, + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "MareSynchronosServices": "Information", + "MareSynchronosShared": "Information", + "System.IO": "Information" + }, + "File": { + "BasePath": "logs", + "FileAccessMode": "KeepOpenAndAutoFlush", + "FileEncodingName": "utf-8", + "DateFormat": "yyyMMdd", + "MaxFileSize": 104857600, + "Files": [ + { + "Path": "///mare--.log" + } + ] + } + }, + "MareSynchronos": { + "DbContextPoolSize": 512, + "ShardName": "Services", + "MetricsPort": 6150, + "CdnFullUrl": "http://localhost:6200/", + "MainServerAddress": "http://mare-server:6000/", + "MainServerGrpcAddress": "http://mare-server:6005/", + "DiscordBotToken": "", + "DiscordChannelForMessages": "", + "Jwt": "teststringteststringteststringteststringteststringteststringteststringteststringteststringteststring", + "RedisConnectionString": "redis,password=secretredispassword" + }, + "AllowedHosts": "*", + "Kestrel": { + }, + "IpRateLimiting": {}, + "IPRateLimitPolicies": {} +} \ No newline at end of file diff --git a/Docker/run/linux-sharded-daemon-start.sh b/Docker/run/linux-sharded-daemon-start.sh new file mode 100644 index 0000000..58b676e --- /dev/null +++ b/Docker/run/linux-sharded-daemon-start.sh @@ -0,0 +1,2 @@ +#!/bin/sh +docker compose -f compose/mare-sharded.yml -p sharded up -d \ No newline at end of file diff --git a/Docker/run/linux-sharded-daemon-stop.sh b/Docker/run/linux-sharded-daemon-stop.sh new file mode 100644 index 0000000..06e0ded --- /dev/null +++ b/Docker/run/linux-sharded-daemon-stop.sh @@ -0,0 +1,2 @@ +#!/bin/sh +docker compose -f compose/mare-sharded.yml -p sharded stop \ No newline at end of file diff --git a/Docker/run/linux-sharded.sh b/Docker/run/linux-sharded.sh new file mode 100644 index 0000000..2f3b570 --- /dev/null +++ b/Docker/run/linux-sharded.sh @@ -0,0 +1,2 @@ +#!/bin/sh +docker compose -f compose/mare-sharded.yml -p sharded up \ No newline at end of file diff --git a/Docker/run/linux-standalone-daemon-start.sh b/Docker/run/linux-standalone-daemon-start.sh new file mode 100644 index 0000000..561d430 --- /dev/null +++ b/Docker/run/linux-standalone-daemon-start.sh @@ -0,0 +1,2 @@ +#!/bin/sh +docker compose -f compose/mare-standalone.yml -p standalone up -d \ No newline at end of file diff --git a/Docker/run/linux-standalone-daemon-stop.sh b/Docker/run/linux-standalone-daemon-stop.sh new file mode 100644 index 0000000..c977cc0 --- /dev/null +++ b/Docker/run/linux-standalone-daemon-stop.sh @@ -0,0 +1,2 @@ +#!/bin/sh +docker compose -f compose/mare-standalone.yml -p standalone stop \ No newline at end of file diff --git a/Docker/run/linux-standalone.sh b/Docker/run/linux-standalone.sh new file mode 100644 index 0000000..a348205 --- /dev/null +++ b/Docker/run/linux-standalone.sh @@ -0,0 +1,2 @@ +#!/bin/sh +docker compose -f compose/mare-standalone.yml -p standalone up \ No newline at end of file diff --git a/Docker/run/windows-sharded-daemon-start.bat b/Docker/run/windows-sharded-daemon-start.bat new file mode 100644 index 0000000..8b132f3 --- /dev/null +++ b/Docker/run/windows-sharded-daemon-start.bat @@ -0,0 +1,2 @@ +@echo off +docker compose -f compose\mare-sharded.yml -p sharded up -d \ No newline at end of file diff --git a/Docker/run/windows-sharded-daemon-stop.bat b/Docker/run/windows-sharded-daemon-stop.bat new file mode 100644 index 0000000..dfa5974 --- /dev/null +++ b/Docker/run/windows-sharded-daemon-stop.bat @@ -0,0 +1,2 @@ +@echo off +docker compose -f compose\mare-sharded.yml -p sharded stop \ No newline at end of file diff --git a/Docker/run/windows-sharded.bat b/Docker/run/windows-sharded.bat new file mode 100644 index 0000000..3f8cde2 --- /dev/null +++ b/Docker/run/windows-sharded.bat @@ -0,0 +1,2 @@ +@echo off +docker compose -f compose\mare-sharded.yml -p sharded up \ No newline at end of file diff --git a/Docker/run/windows-standalone-daemon-start.bat b/Docker/run/windows-standalone-daemon-start.bat new file mode 100644 index 0000000..6605d6d --- /dev/null +++ b/Docker/run/windows-standalone-daemon-start.bat @@ -0,0 +1,2 @@ +@echo off +docker compose -f compose\mare-standalone.yml -p standalone up -d \ No newline at end of file diff --git a/Docker/run/windows-standalone-daemon-stop.bat b/Docker/run/windows-standalone-daemon-stop.bat new file mode 100644 index 0000000..2b725f4 --- /dev/null +++ b/Docker/run/windows-standalone-daemon-stop.bat @@ -0,0 +1,2 @@ +@echo off +docker compose -f compose\mare-standalone.yml -p standalone stop \ No newline at end of file diff --git a/Docker/run/windows-standalone.bat b/Docker/run/windows-standalone.bat new file mode 100644 index 0000000..747225f --- /dev/null +++ b/Docker/run/windows-standalone.bat @@ -0,0 +1,2 @@ +@echo off +docker compose -f compose\mare-standalone.yml -p standalone up \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f0d7bf5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Mare Synchronos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MareAPI/.gitignore b/MareAPI/.gitignore new file mode 100644 index 0000000..dfcfd56 --- /dev/null +++ b/MareAPI/.gitignore @@ -0,0 +1,350 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ diff --git a/MareAPI/LICENSE b/MareAPI/LICENSE new file mode 100644 index 0000000..f0d7bf5 --- /dev/null +++ b/MareAPI/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Mare Synchronos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MareAPI/MareSynchronosAPI/Data/CharacterData.cs b/MareAPI/MareSynchronosAPI/Data/CharacterData.cs new file mode 100644 index 0000000..da155a3 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/CharacterData.cs @@ -0,0 +1,36 @@ +using MareSynchronos.API.Data.Enum; +using MessagePack; +using System.Text.Json.Serialization; +using System.Text.Json; +using System.Text; +using System.Security.Cryptography; + +namespace MareSynchronos.API.Data; + +[MessagePackObject(keyAsPropertyName: true)] +public class CharacterData +{ + public CharacterData() + { + DataHash = new(() => + { + var json = JsonSerializer.Serialize(this); +#pragma warning disable SYSLIB0021 // Type or member is obsolete + using SHA256CryptoServiceProvider cryptoProvider = new(); +#pragma warning restore SYSLIB0021 // Type or member is obsolete + return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(json))).Replace("-", "", StringComparison.Ordinal); + }); + } + + public Dictionary CustomizePlusData { get; set; } = new(); + [JsonIgnore] + public Lazy DataHash { get; } + + public Dictionary> FileReplacements { get; set; } = new(); + public Dictionary GlamourerData { get; set; } = new(); + public string HeelsData { get; set; } = string.Empty; + public string HonorificData { get; set; } = string.Empty; + public string ManipulationData { get; set; } = string.Empty; + public string MoodlesData { get; set; } = string.Empty; + public string PetNamesData { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/ChatMessage.cs b/MareAPI/MareSynchronosAPI/Data/ChatMessage.cs new file mode 100644 index 0000000..55224a5 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/ChatMessage.cs @@ -0,0 +1,11 @@ +using MessagePack; + +namespace MareSynchronos.API.Data; + +[MessagePackObject(keyAsPropertyName: true)] +public record ChatMessage +{ + public string SenderName { get; set; } = string.Empty; + public uint SenderHomeWorldId { get; set; } = 0; + public byte[] PayloadContent { get; set; } = []; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Comparer/GroupDataComparer.cs b/MareAPI/MareSynchronosAPI/Data/Comparer/GroupDataComparer.cs new file mode 100644 index 0000000..dfd0456 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Comparer/GroupDataComparer.cs @@ -0,0 +1,19 @@ +namespace MareSynchronos.API.Data.Comparer; + +public class GroupDataComparer : IEqualityComparer +{ + public static GroupDataComparer Instance => _instance; + private static GroupDataComparer _instance = new GroupDataComparer(); + + private GroupDataComparer() { } + public bool Equals(GroupData? x, GroupData? y) + { + if (x == null || y == null) return false; + return x.GID.Equals(y.GID, StringComparison.Ordinal); + } + + public int GetHashCode(GroupData obj) + { + return obj.GID.GetHashCode(); + } +} diff --git a/MareAPI/MareSynchronosAPI/Data/Comparer/GroupDtoComparer.cs b/MareAPI/MareSynchronosAPI/Data/Comparer/GroupDtoComparer.cs new file mode 100644 index 0000000..3814c6f --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Comparer/GroupDtoComparer.cs @@ -0,0 +1,23 @@ +using MareSynchronos.API.Dto.Group; + +namespace MareSynchronos.API.Data.Comparer; + + +public class GroupDtoComparer : IEqualityComparer +{ + public static GroupDtoComparer Instance => _instance; + private static GroupDtoComparer _instance = new GroupDtoComparer(); + + private GroupDtoComparer() { } + + public bool Equals(GroupDto? x, GroupDto? y) + { + if (x == null || y == null) return false; + return x.GID.Equals(y.GID, StringComparison.Ordinal); + } + + public int GetHashCode(GroupDto obj) + { + return obj.Group.GID.GetHashCode(); + } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Comparer/GroupPairDtoComparer.cs b/MareAPI/MareSynchronosAPI/Data/Comparer/GroupPairDtoComparer.cs new file mode 100644 index 0000000..c1dde50 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Comparer/GroupPairDtoComparer.cs @@ -0,0 +1,20 @@ +using MareSynchronos.API.Dto.Group; + +namespace MareSynchronos.API.Data.Comparer; + +public class GroupPairDtoComparer : IEqualityComparer +{ + public static GroupPairDtoComparer Instance => _instance; + private static GroupPairDtoComparer _instance = new(); + private GroupPairDtoComparer() { } + public bool Equals(GroupPairDto? x, GroupPairDto? y) + { + if (x == null || y == null) return false; + return x.GID.Equals(y.GID, StringComparison.Ordinal) && x.UID.Equals(y.UID, StringComparison.Ordinal); + } + + public int GetHashCode(GroupPairDto obj) + { + return HashCode.Combine(obj.Group.GID.GetHashCode(), obj.User.UID.GetHashCode()); + } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Comparer/UserDataComparer.cs b/MareAPI/MareSynchronosAPI/Data/Comparer/UserDataComparer.cs new file mode 100644 index 0000000..68aa227 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Comparer/UserDataComparer.cs @@ -0,0 +1,20 @@ +namespace MareSynchronos.API.Data.Comparer; + +public class UserDataComparer : IEqualityComparer +{ + public static UserDataComparer Instance => _instance; + private static UserDataComparer _instance = new(); + + private UserDataComparer() { } + + public bool Equals(UserData? x, UserData? y) + { + if (x == null || y == null) return false; + return x.UID.Equals(y.UID, StringComparison.Ordinal); + } + + public int GetHashCode(UserData obj) + { + return obj.UID.GetHashCode(); + } +} diff --git a/MareAPI/MareSynchronosAPI/Data/Comparer/UserDtoComparer.cs b/MareAPI/MareSynchronosAPI/Data/Comparer/UserDtoComparer.cs new file mode 100644 index 0000000..9c8451c --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Comparer/UserDtoComparer.cs @@ -0,0 +1,20 @@ +using MareSynchronos.API.Dto.User; + +namespace MareSynchronos.API.Data.Comparer; + +public class UserDtoComparer : IEqualityComparer +{ + public static UserDtoComparer Instance => _instance; + private static UserDtoComparer _instance = new(); + private UserDtoComparer() { } + public bool Equals(UserDto? x, UserDto? y) + { + if (x == null || y == null) return false; + return x.User.UID.Equals(y.User.UID, StringComparison.Ordinal); + } + + public int GetHashCode(UserDto obj) + { + return obj.User.UID.GetHashCode(); + } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Enum/GroupPermissions.cs b/MareAPI/MareSynchronosAPI/Data/Enum/GroupPermissions.cs new file mode 100644 index 0000000..cccc712 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Enum/GroupPermissions.cs @@ -0,0 +1,11 @@ +namespace MareSynchronos.API.Data.Enum; + +[Flags] +public enum GroupPermissions +{ + NoneSet = 0x0, + DisableAnimations = 0x1, + DisableSounds = 0x2, + DisableInvites = 0x4, + DisableVFX = 0x8, +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Enum/GroupUserInfo.cs b/MareAPI/MareSynchronosAPI/Data/Enum/GroupUserInfo.cs new file mode 100644 index 0000000..ed1b3bb --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Enum/GroupUserInfo.cs @@ -0,0 +1,9 @@ +namespace MareSynchronos.API.Data.Enum; + +[Flags] +public enum GroupUserInfo +{ + None = 0x0, + IsModerator = 0x2, + IsPinned = 0x4 +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Enum/GroupUserPermissions.cs b/MareAPI/MareSynchronosAPI/Data/Enum/GroupUserPermissions.cs new file mode 100644 index 0000000..efa3bfd --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Enum/GroupUserPermissions.cs @@ -0,0 +1,11 @@ +namespace MareSynchronos.API.Data.Enum; + +[Flags] +public enum GroupUserPermissions +{ + NoneSet = 0x0, + Paused = 0x1, + DisableAnimations = 0x2, + DisableSounds = 0x4, + DisableVFX = 0x8, +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Enum/MessageSeverity.cs b/MareAPI/MareSynchronosAPI/Data/Enum/MessageSeverity.cs new file mode 100644 index 0000000..b0ace02 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Enum/MessageSeverity.cs @@ -0,0 +1,8 @@ +namespace MareSynchronos.API.Data.Enum; + +public enum MessageSeverity +{ + Information, + Warning, + Error +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Enum/ObjectKind.cs b/MareAPI/MareSynchronosAPI/Data/Enum/ObjectKind.cs new file mode 100644 index 0000000..47396c4 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Enum/ObjectKind.cs @@ -0,0 +1,9 @@ +namespace MareSynchronos.API.Data.Enum; + +public enum ObjectKind +{ + Player = 0, + MinionOrMount = 1, + Companion = 2, + Pet = 3, +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Enum/UserPermissions.cs b/MareAPI/MareSynchronosAPI/Data/Enum/UserPermissions.cs new file mode 100644 index 0000000..8cc472b --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Enum/UserPermissions.cs @@ -0,0 +1,12 @@ +namespace MareSynchronos.API.Data.Enum; + +[Flags] +public enum UserPermissions +{ + NoneSet = 0, + Paired = 1, + Paused = 2, + DisableAnimations = 4, + DisableSounds = 8, + DisableVFX = 16, +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Extensions/GroupPermissionsExtensions.cs b/MareAPI/MareSynchronosAPI/Data/Extensions/GroupPermissionsExtensions.cs new file mode 100644 index 0000000..ca2236d --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Extensions/GroupPermissionsExtensions.cs @@ -0,0 +1,50 @@ +using MareSynchronos.API.Data.Enum; + +namespace MareSynchronos.API.Data.Extensions; + +public static class GroupPermissionsExtensions +{ + public static bool IsDisableAnimations(this GroupPermissions perm) + { + return perm.HasFlag(GroupPermissions.DisableAnimations); + } + + public static bool IsDisableSounds(this GroupPermissions perm) + { + return perm.HasFlag(GroupPermissions.DisableSounds); + } + + public static bool IsDisableInvites(this GroupPermissions perm) + { + return perm.HasFlag(GroupPermissions.DisableInvites); + } + + public static bool IsDisableVFX(this GroupPermissions perm) + { + return perm.HasFlag(GroupPermissions.DisableVFX); + } + + public static void SetDisableAnimations(this ref GroupPermissions perm, bool set) + { + if (set) perm |= GroupPermissions.DisableAnimations; + else perm &= ~GroupPermissions.DisableAnimations; + } + + public static void SetDisableSounds(this ref GroupPermissions perm, bool set) + { + if (set) perm |= GroupPermissions.DisableSounds; + else perm &= ~GroupPermissions.DisableSounds; + } + + public static void SetDisableInvites(this ref GroupPermissions perm, bool set) + { + if (set) perm |= GroupPermissions.DisableInvites; + else perm &= ~GroupPermissions.DisableInvites; + } + + public static void SetDisableVFX(this ref GroupPermissions perm, bool set) + { + if (set) perm |= GroupPermissions.DisableVFX; + else perm &= ~GroupPermissions.DisableVFX; + } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Extensions/GroupUserInfoExtensions.cs b/MareAPI/MareSynchronosAPI/Data/Extensions/GroupUserInfoExtensions.cs new file mode 100644 index 0000000..a4608e8 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Extensions/GroupUserInfoExtensions.cs @@ -0,0 +1,28 @@ +using MareSynchronos.API.Data.Enum; + +namespace MareSynchronos.API.Data.Extensions; + +public static class GroupUserInfoExtensions +{ + public static bool IsModerator(this GroupUserInfo info) + { + return info.HasFlag(GroupUserInfo.IsModerator); + } + + public static bool IsPinned(this GroupUserInfo info) + { + return info.HasFlag(GroupUserInfo.IsPinned); + } + + public static void SetModerator(this ref GroupUserInfo info, bool isModerator) + { + if (isModerator) info |= GroupUserInfo.IsModerator; + else info &= ~GroupUserInfo.IsModerator; + } + + public static void SetPinned(this ref GroupUserInfo info, bool isPinned) + { + if (isPinned) info |= GroupUserInfo.IsPinned; + else info &= ~GroupUserInfo.IsPinned; + } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Extensions/GroupUserPermissionsExtensions.cs b/MareAPI/MareSynchronosAPI/Data/Extensions/GroupUserPermissionsExtensions.cs new file mode 100644 index 0000000..b8b2702 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Extensions/GroupUserPermissionsExtensions.cs @@ -0,0 +1,50 @@ +using MareSynchronos.API.Data.Enum; + +namespace MareSynchronos.API.Data.Extensions; + +public static class GroupUserPermissionsExtensions +{ + public static bool IsDisableAnimations(this GroupUserPermissions perm) + { + return perm.HasFlag(GroupUserPermissions.DisableAnimations); + } + + public static bool IsDisableSounds(this GroupUserPermissions perm) + { + return perm.HasFlag(GroupUserPermissions.DisableSounds); + } + + public static bool IsPaused(this GroupUserPermissions perm) + { + return perm.HasFlag(GroupUserPermissions.Paused); + } + + public static bool IsDisableVFX(this GroupUserPermissions perm) + { + return perm.HasFlag(GroupUserPermissions.DisableVFX); + } + + public static void SetDisableAnimations(this ref GroupUserPermissions perm, bool set) + { + if (set) perm |= GroupUserPermissions.DisableAnimations; + else perm &= ~GroupUserPermissions.DisableAnimations; + } + + public static void SetDisableSounds(this ref GroupUserPermissions perm, bool set) + { + if (set) perm |= GroupUserPermissions.DisableSounds; + else perm &= ~GroupUserPermissions.DisableSounds; + } + + public static void SetPaused(this ref GroupUserPermissions perm, bool set) + { + if (set) perm |= GroupUserPermissions.Paused; + else perm &= ~GroupUserPermissions.Paused; + } + + public static void SetDisableVFX(this ref GroupUserPermissions perm, bool set) + { + if (set) perm |= GroupUserPermissions.DisableVFX; + else perm &= ~GroupUserPermissions.DisableVFX; + } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Extensions/UserPermissionsExtensions.cs b/MareAPI/MareSynchronosAPI/Data/Extensions/UserPermissionsExtensions.cs new file mode 100644 index 0000000..2b80601 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Extensions/UserPermissionsExtensions.cs @@ -0,0 +1,61 @@ +using MareSynchronos.API.Data.Enum; + +namespace MareSynchronos.API.Data.Extensions; + +public static class UserPermissionsExtensions +{ + public static bool IsPaired(this UserPermissions perm) + { + return perm.HasFlag(UserPermissions.Paired); + } + + public static bool IsPaused(this UserPermissions perm) + { + return perm.HasFlag(UserPermissions.Paused); + } + + public static bool IsDisableAnimations(this UserPermissions perm) + { + return perm.HasFlag(UserPermissions.DisableAnimations); + } + + public static bool IsDisableSounds(this UserPermissions perm) + { + return perm.HasFlag(UserPermissions.DisableSounds); + } + + public static bool IsDisableVFX(this UserPermissions perm) + { + return perm.HasFlag(UserPermissions.DisableVFX); + } + + public static void SetPaired(this ref UserPermissions perm, bool paired) + { + if (paired) perm |= UserPermissions.Paired; + else perm &= ~UserPermissions.Paired; + } + + public static void SetPaused(this ref UserPermissions perm, bool paused) + { + if (paused) perm |= UserPermissions.Paused; + else perm &= ~UserPermissions.Paused; + } + + public static void SetDisableAnimations(this ref UserPermissions perm, bool set) + { + if (set) perm |= UserPermissions.DisableAnimations; + else perm &= ~UserPermissions.DisableAnimations; + } + + public static void SetDisableSounds(this ref UserPermissions perm, bool set) + { + if (set) perm |= UserPermissions.DisableSounds; + else perm &= ~UserPermissions.DisableSounds; + } + + public static void SetDisableVFX(this ref UserPermissions perm, bool set) + { + if (set) perm |= UserPermissions.DisableVFX; + else perm &= ~UserPermissions.DisableVFX; + } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/FileReplacementData.cs b/MareAPI/MareSynchronosAPI/Data/FileReplacementData.cs new file mode 100644 index 0000000..82161a5 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/FileReplacementData.cs @@ -0,0 +1,30 @@ +using MessagePack; +using System.Text.Json.Serialization; +using System.Text.Json; +using System.Text; +using System.Security.Cryptography; + + +namespace MareSynchronos.API.Data; + +[MessagePackObject(keyAsPropertyName: true)] +public class FileReplacementData +{ + public FileReplacementData() + { + DataHash = new(() => + { + var json = JsonSerializer.Serialize(this); +#pragma warning disable SYSLIB0021 // Type or member is obsolete + using SHA256CryptoServiceProvider cryptoProvider = new(); +#pragma warning restore SYSLIB0021 // Type or member is obsolete + return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(json))).Replace("-", "", StringComparison.Ordinal); + }); + } + + [JsonIgnore] + public Lazy DataHash { get; } + public string[] GamePaths { get; set; } = Array.Empty(); + public string Hash { get; set; } = string.Empty; + public string FileSwapPath { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/GroupData.cs b/MareAPI/MareSynchronosAPI/Data/GroupData.cs new file mode 100644 index 0000000..877bb44 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/GroupData.cs @@ -0,0 +1,10 @@ +using MessagePack; + +namespace MareSynchronos.API.Data; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupData(string GID, string? Alias = null) +{ + [IgnoreMember] + public string AliasOrGID => string.IsNullOrWhiteSpace(Alias) ? GID : Alias; +} diff --git a/MareAPI/MareSynchronosAPI/Data/SignedChatMessage.cs b/MareAPI/MareSynchronosAPI/Data/SignedChatMessage.cs new file mode 100644 index 0000000..edfd8cc --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/SignedChatMessage.cs @@ -0,0 +1,14 @@ +using MessagePack; + +namespace MareSynchronos.API.Data; + +[MessagePackObject(keyAsPropertyName: true)] +public record SignedChatMessage(ChatMessage Message, UserData Sender) : ChatMessage(Message) +{ + // Sender and timestamp are set by the server + public UserData Sender { get; set; } = Sender; + public long Timestamp { get; set; } = 0; + // Signature is generated by the server as SHA256(Sender.UID | Timestamp | Destination | Message) + // Where Destination is either the receiver's UID, or the group GID + public string Signature { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/UserData.cs b/MareAPI/MareSynchronosAPI/Data/UserData.cs new file mode 100644 index 0000000..3bc74cf --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/UserData.cs @@ -0,0 +1,10 @@ +using MessagePack; + +namespace MareSynchronos.API.Data; + +[MessagePackObject(keyAsPropertyName: true)] +public record UserData(string UID, string? Alias = null) +{ + [IgnoreMember] + public string AliasOrUID => string.IsNullOrWhiteSpace(Alias) ? UID : Alias; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Account/RegisterReplyDto.cs b/MareAPI/MareSynchronosAPI/Dto/Account/RegisterReplyDto.cs new file mode 100644 index 0000000..ce3f741 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Account/RegisterReplyDto.cs @@ -0,0 +1,12 @@ +using MessagePack; + +namespace MareSynchronos.API.Dto.Account; + +[MessagePackObject(keyAsPropertyName: true)] +public record RegisterReplyDto +{ + public bool Success { get; set; } = false; + public string ErrorMessage { get; set; } = string.Empty; + public string UID { get; set; } = string.Empty; + public string SecretKey { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Account/RegisterReplyV2Dto.cs b/MareAPI/MareSynchronosAPI/Dto/Account/RegisterReplyV2Dto.cs new file mode 100644 index 0000000..59f7fe5 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Account/RegisterReplyV2Dto.cs @@ -0,0 +1,11 @@ +using MessagePack; + +namespace MareSynchronos.API.Dto.Account; + +[MessagePackObject(keyAsPropertyName: true)] +public record RegisterReplyV2Dto +{ + public bool Success { get; set; } = false; + public string ErrorMessage { get; set; } = string.Empty; + public string UID { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/AuthReplyDto.cs b/MareAPI/MareSynchronosAPI/Dto/AuthReplyDto.cs new file mode 100644 index 0000000..d3033fd --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/AuthReplyDto.cs @@ -0,0 +1,11 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto; + +[MessagePackObject(keyAsPropertyName: true)] +public record AuthReplyDto +{ + public string Token { get; set; } = string.Empty; + public string? WellKnown { get; set; } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/CharaData/AccessTypeDto.cs b/MareAPI/MareSynchronosAPI/Dto/CharaData/AccessTypeDto.cs new file mode 100644 index 0000000..9c53eaa --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/CharaData/AccessTypeDto.cs @@ -0,0 +1,9 @@ +namespace MareSynchronos.API.Dto.CharaData; + +public enum AccessTypeDto +{ + Individuals, + ClosePairs, + AllPairs, + Public +} diff --git a/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataDownloadDto.cs b/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataDownloadDto.cs new file mode 100644 index 0000000..5d450b8 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataDownloadDto.cs @@ -0,0 +1,14 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.CharaData; + +[MessagePackObject(keyAsPropertyName: true)] +public record CharaDataDownloadDto(string Id, UserData Uploader) : CharaDataDto(Id, Uploader) +{ + public string GlamourerData { get; init; } = string.Empty; + public string CustomizeData { get; init; } = string.Empty; + public string ManipulationData { get; set; } = string.Empty; + public List FileGamePaths { get; init; } = []; + public List FileSwaps { get; init; } = []; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataDto.cs b/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataDto.cs new file mode 100644 index 0000000..dbf4a26 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataDto.cs @@ -0,0 +1,9 @@ +using MareSynchronos.API.Data; + +namespace MareSynchronos.API.Dto.CharaData; + +public record CharaDataDto(string Id, UserData Uploader) +{ + public string Description { get; init; } = string.Empty; + public DateTime UpdatedDate { get; init; } +} diff --git a/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataFullDto.cs b/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataFullDto.cs new file mode 100644 index 0000000..d8b4016 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataFullDto.cs @@ -0,0 +1,88 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.CharaData; + +[MessagePackObject(keyAsPropertyName: true)] +public record CharaDataFullDto(string Id, UserData Uploader) : CharaDataDto(Id, Uploader) +{ + public DateTime CreatedDate { get; init; } + public DateTime ExpiryDate { get; set; } + public string GlamourerData { get; set; } = string.Empty; + public string CustomizeData { get; set; } = string.Empty; + public string ManipulationData { get; set; } = string.Empty; + public int DownloadCount { get; set; } = 0; + public List AllowedUsers { get; set; } = []; + public List AllowedGroups { get; set; } = []; + public List FileGamePaths { get; set; } = []; + public List FileSwaps { get; set; } = []; + public List OriginalFiles { get; set; } = []; + public AccessTypeDto AccessType { get; set; } + public ShareTypeDto ShareType { get; set; } + public List PoseData { get; set; } = []; +} + +[MessagePackObject(keyAsPropertyName: true)] +public record GamePathEntry(string HashOrFileSwap, string GamePath); + +[MessagePackObject(keyAsPropertyName: true)] +public record PoseEntry(long? Id) +{ + public string? Description { get; set; } = string.Empty; + public string? PoseData { get; set; } = string.Empty; + public WorldData? WorldData { get; set; } +} + +[MessagePackObject] +public record struct WorldData +{ + [Key(0)] public LocationInfo LocationInfo { get; set; } + [Key(1)] public float PositionX { get; set; } + [Key(2)] public float PositionY { get; set; } + [Key(3)] public float PositionZ { get; set; } + [Key(4)] public float RotationX { get; set; } + [Key(5)] public float RotationY { get; set; } + [Key(6)] public float RotationZ { get; set; } + [Key(7)] public float RotationW { get; set; } + [Key(8)] public float ScaleX { get; set; } + [Key(9)] public float ScaleY { get; set; } + [Key(10)] public float ScaleZ { get; set; } +} + +[MessagePackObject] +public record struct LocationInfo +{ + [Key(0)] public uint ServerId { get; set; } + [Key(1)] public uint MapId { get; set; } + [Key(2)] public uint TerritoryId { get; set; } + [Key(3)] public uint DivisionId { get; set; } + [Key(4)] public uint WardId { get; set; } + [Key(5)] public uint HouseId { get; set; } + [Key(6)] public uint RoomId { get; set; } +} + +[MessagePackObject] +public record struct PoseData +{ + [Key(0)] public bool IsDelta { get; set; } + [Key(1)] public Dictionary Bones { get; set; } + [Key(2)] public Dictionary MainHand { get; set; } + [Key(3)] public Dictionary OffHand { get; set; } + [Key(4)] public BoneData ModelDifference { get; set; } +} + +[MessagePackObject] +public record struct BoneData +{ + [Key(0)] public bool Exists { get; set; } + [Key(1)] public float PositionX { get; set; } + [Key(2)] public float PositionY { get; set; } + [Key(3)] public float PositionZ { get; set; } + [Key(4)] public float RotationX { get; set; } + [Key(5)] public float RotationY { get; set; } + [Key(6)] public float RotationZ { get; set; } + [Key(7)] public float RotationW { get; set; } + [Key(8)] public float ScaleX { get; set; } + [Key(9)] public float ScaleY { get; set; } + [Key(10)] public float ScaleZ { get; set; } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataMetaInfoDto.cs b/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataMetaInfoDto.cs new file mode 100644 index 0000000..7afb6b2 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataMetaInfoDto.cs @@ -0,0 +1,11 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.CharaData; + +[MessagePackObject(keyAsPropertyName: true)] +public record CharaDataMetaInfoDto(string Id, UserData Uploader) : CharaDataDto(Id, Uploader) +{ + public bool CanBeDownloaded { get; init; } + public List PoseData { get; set; } = []; +} diff --git a/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataUpdateDto.cs b/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataUpdateDto.cs new file mode 100644 index 0000000..30d1348 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataUpdateDto.cs @@ -0,0 +1,20 @@ +using MessagePack; + +namespace MareSynchronos.API.Dto.CharaData; + +[MessagePackObject(keyAsPropertyName: true)] +public record CharaDataUpdateDto(string Id) +{ + public string? Description { get; set; } + public DateTime? ExpiryDate { get; set; } + public string? GlamourerData { get; set; } + public string? CustomizeData { get; set; } + public string? ManipulationData { get; set; } + public List? AllowedUsers { get; set; } + public List? AllowedGroups { get; set; } + public List? FileGamePaths { get; set; } + public List? FileSwaps { get; set; } + public AccessTypeDto? AccessType { get; set; } + public ShareTypeDto? ShareType { get; set; } + public List? Poses { get; set; } +} diff --git a/MareAPI/MareSynchronosAPI/Dto/CharaData/ShareTypeDto.cs b/MareAPI/MareSynchronosAPI/Dto/CharaData/ShareTypeDto.cs new file mode 100644 index 0000000..ed55f94 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/CharaData/ShareTypeDto.cs @@ -0,0 +1,7 @@ +namespace MareSynchronos.API.Dto.CharaData; + +public enum ShareTypeDto +{ + Private, + Shared +} diff --git a/MareAPI/MareSynchronosAPI/Dto/Chat/GroupChatMsgDto.cs b/MareAPI/MareSynchronosAPI/Dto/Chat/GroupChatMsgDto.cs new file mode 100644 index 0000000..c946c00 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Chat/GroupChatMsgDto.cs @@ -0,0 +1,13 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.API.Dto.User; +using MessagePack; + +namespace MareSynchronos.API.Dto.Chat; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupChatMsgDto(GroupDto Group, SignedChatMessage Message) +{ + public GroupDto Group = Group; + public SignedChatMessage Message = Message; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Chat/UserChatMsgDto.cs b/MareAPI/MareSynchronosAPI/Dto/Chat/UserChatMsgDto.cs new file mode 100644 index 0000000..d82855b --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Chat/UserChatMsgDto.cs @@ -0,0 +1,11 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Dto.User; +using MessagePack; + +namespace MareSynchronos.API.Dto.Chat; + +[MessagePackObject(keyAsPropertyName: true)] +public record UserChatMsgDto(SignedChatMessage Message) +{ + public SignedChatMessage Message = Message; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/ConnectionDto.cs b/MareAPI/MareSynchronosAPI/Dto/ConnectionDto.cs new file mode 100644 index 0000000..04c818e --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/ConnectionDto.cs @@ -0,0 +1,25 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto; + +[MessagePackObject(keyAsPropertyName: true)] +public record ConnectionDto(UserData User) +{ + public Version CurrentClientVersion { get; set; } = new(0, 0, 0); + public int ServerVersion { get; set; } + public bool IsAdmin { get; set; } + public bool IsModerator { get; set; } + public ServerInfo ServerInfo { get; set; } = new(); +} + +[MessagePackObject(keyAsPropertyName: true)] +public record ServerInfo +{ + public string ShardName { get; set; } = string.Empty; + public int MaxGroupUserCount { get; set; } + public int MaxGroupsCreatedByUser { get; set; } + public int MaxGroupsJoinedByUser { get; set; } + public Uri FileServerAddress { get; set; } = new Uri("http://nonemptyuri"); + public int MaxCharaData { get; set; } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Files/DownloadFileDto.cs b/MareAPI/MareSynchronosAPI/Dto/Files/DownloadFileDto.cs new file mode 100644 index 0000000..d2ffe05 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Files/DownloadFileDto.cs @@ -0,0 +1,14 @@ +using MessagePack; + +namespace MareSynchronos.API.Dto.Files; + +[MessagePackObject(keyAsPropertyName: true)] +public record DownloadFileDto : ITransferFileDto +{ + public bool FileExists { get; set; } = true; + public string Hash { get; set; } = string.Empty; + public string Url { get; set; } = string.Empty; + public long Size { get; set; } = 0; + public bool IsForbidden { get; set; } = false; + public string ForbiddenBy { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Files/FilesSendDto.cs b/MareAPI/MareSynchronosAPI/Dto/Files/FilesSendDto.cs new file mode 100644 index 0000000..b7a6735 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Files/FilesSendDto.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MareSynchronos.API.Dto.Files; + +public class FilesSendDto +{ + public List FileHashes { get; set; } = new(); + public List UIDs { get; set; } = new(); +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Files/ITransferFileDto.cs b/MareAPI/MareSynchronosAPI/Dto/Files/ITransferFileDto.cs new file mode 100644 index 0000000..fb20e5a --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Files/ITransferFileDto.cs @@ -0,0 +1,8 @@ +namespace MareSynchronos.API.Dto.Files; + +public interface ITransferFileDto +{ + string Hash { get; set; } + bool IsForbidden { get; set; } + string ForbiddenBy { get; set; } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Files/UploadFileDto.cs b/MareAPI/MareSynchronosAPI/Dto/Files/UploadFileDto.cs new file mode 100644 index 0000000..f10b27d --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Files/UploadFileDto.cs @@ -0,0 +1,11 @@ +using MessagePack; + +namespace MareSynchronos.API.Dto.Files; + +[MessagePackObject(keyAsPropertyName: true)] +public record UploadFileDto : ITransferFileDto +{ + public string Hash { get; set; } = string.Empty; + public bool IsForbidden { get; set; } = false; + public string ForbiddenBy { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Group/BannedGroupUserDto.cs b/MareAPI/MareSynchronosAPI/Dto/Group/BannedGroupUserDto.cs new file mode 100644 index 0000000..36ed1f9 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Group/BannedGroupUserDto.cs @@ -0,0 +1,19 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record BannedGroupUserDto : GroupPairDto +{ + public BannedGroupUserDto(GroupData group, UserData user, string reason, DateTime bannedOn, string bannedBy) : base(group, user) + { + Reason = reason; + BannedOn = bannedOn; + BannedBy = bannedBy; + } + + public string Reason { get; set; } + public DateTime BannedOn { get; set; } + public string BannedBy { get; set; } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Group/GroupDto.cs b/MareAPI/MareSynchronosAPI/Dto/Group/GroupDto.cs new file mode 100644 index 0000000..5b5b71a --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Group/GroupDto.cs @@ -0,0 +1,13 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupDto(GroupData Group) +{ + public GroupData Group { get; set; } = Group; + public string GID => Group.GID; + public string? GroupAlias => Group.Alias; + public string GroupAliasOrGID => Group.AliasOrGID; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Group/GroupFullInfoDto.cs b/MareAPI/MareSynchronosAPI/Dto/Group/GroupFullInfoDto.cs new file mode 100644 index 0000000..0591293 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Group/GroupFullInfoDto.cs @@ -0,0 +1,12 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MessagePack; + +namespace MareSynchronos.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupFullInfoDto(GroupData Group, UserData Owner, GroupPermissions GroupPermissions, GroupUserPermissions GroupUserPermissions, GroupUserInfo GroupUserInfo) : GroupInfoDto(Group, Owner, GroupPermissions) +{ + public GroupUserPermissions GroupUserPermissions { get; set; } = GroupUserPermissions; + public GroupUserInfo GroupUserInfo { get; set; } = GroupUserInfo; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Group/GroupInfoDto.cs b/MareAPI/MareSynchronosAPI/Dto/Group/GroupInfoDto.cs new file mode 100644 index 0000000..193072b --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Group/GroupInfoDto.cs @@ -0,0 +1,16 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MessagePack; + +namespace MareSynchronos.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupInfoDto(GroupData Group, UserData Owner, GroupPermissions GroupPermissions) : GroupDto(Group) +{ + public GroupPermissions GroupPermissions { get; set; } = GroupPermissions; + public UserData Owner { get; set; } = Owner; + + public string OwnerUID => Owner.UID; + public string? OwnerAlias => Owner.Alias; + public string OwnerAliasOrUID => Owner.AliasOrUID; +} diff --git a/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairDto.cs b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairDto.cs new file mode 100644 index 0000000..c2e748d --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairDto.cs @@ -0,0 +1,12 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupPairDto(GroupData Group, UserData User) : GroupDto(Group) +{ + public string UID => User.UID; + public string? UserAlias => User.Alias; + public string UserAliasOrUID => User.AliasOrUID; +} diff --git a/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairFullInfoDto.cs b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairFullInfoDto.cs new file mode 100644 index 0000000..5a594df --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairFullInfoDto.cs @@ -0,0 +1,12 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MessagePack; + +namespace MareSynchronos.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupPairFullInfoDto(GroupData Group, UserData User, GroupUserInfo GroupPairStatusInfo, GroupUserPermissions GroupUserPermissions) : GroupPairDto(Group, User) +{ + public GroupUserInfo GroupPairStatusInfo { get; set; } = GroupPairStatusInfo; + public GroupUserPermissions GroupUserPermissions { get; set; } = GroupUserPermissions; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairUserInfoDto.cs b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairUserInfoDto.cs new file mode 100644 index 0000000..8a37f68 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairUserInfoDto.cs @@ -0,0 +1,8 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MessagePack; + +namespace MareSynchronos.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupPairUserInfoDto(GroupData Group, UserData User, GroupUserInfo GroupUserInfo) : GroupPairDto(Group, User); diff --git a/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairUserPermissionDto.cs b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairUserPermissionDto.cs new file mode 100644 index 0000000..d1f152f --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairUserPermissionDto.cs @@ -0,0 +1,8 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MessagePack; + +namespace MareSynchronos.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupPairUserPermissionDto(GroupData Group, UserData User, GroupUserPermissions GroupPairPermissions) : GroupPairDto(Group, User); diff --git a/MareAPI/MareSynchronosAPI/Dto/Group/GroupPasswordDto.cs b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPasswordDto.cs new file mode 100644 index 0000000..bcc31f0 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPasswordDto.cs @@ -0,0 +1,7 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupPasswordDto(GroupData Group, string Password) : GroupDto(Group); diff --git a/MareAPI/MareSynchronosAPI/Dto/Group/GroupPermissionDto.cs b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPermissionDto.cs new file mode 100644 index 0000000..70dbf80 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPermissionDto.cs @@ -0,0 +1,8 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MessagePack; + +namespace MareSynchronos.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupPermissionDto(GroupData Group, GroupPermissions Permissions) : GroupDto(Group); diff --git a/MareAPI/MareSynchronosAPI/Dto/SystemInfoDto.cs b/MareAPI/MareSynchronosAPI/Dto/SystemInfoDto.cs new file mode 100644 index 0000000..eb84f1a --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/SystemInfoDto.cs @@ -0,0 +1,9 @@ +using MessagePack; + +namespace MareSynchronos.API.Dto; + +[MessagePackObject(keyAsPropertyName: true)] +public record SystemInfoDto +{ + public int OnlineUsers { get; set; } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/User/OnlineUserCharaDataDto.cs b/MareAPI/MareSynchronosAPI/Dto/User/OnlineUserCharaDataDto.cs new file mode 100644 index 0000000..a4233d5 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/User/OnlineUserCharaDataDto.cs @@ -0,0 +1,7 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record OnlineUserCharaDataDto(UserData User, CharacterData CharaData) : UserDto(User); \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/User/OnlineUserIdentDto.cs b/MareAPI/MareSynchronosAPI/Dto/User/OnlineUserIdentDto.cs new file mode 100644 index 0000000..dbc7129 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/User/OnlineUserIdentDto.cs @@ -0,0 +1,7 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record OnlineUserIdentDto(UserData User, string Ident) : UserDto(User); \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/User/UserCharaDataMessageDto.cs b/MareAPI/MareSynchronosAPI/Dto/User/UserCharaDataMessageDto.cs new file mode 100644 index 0000000..1b33590 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/User/UserCharaDataMessageDto.cs @@ -0,0 +1,7 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record UserCharaDataMessageDto(List Recipients, CharacterData CharaData); \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/User/UserDto.cs b/MareAPI/MareSynchronosAPI/Dto/User/UserDto.cs new file mode 100644 index 0000000..ce105bf --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/User/UserDto.cs @@ -0,0 +1,7 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record UserDto(UserData User); \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/User/UserPairDto.cs b/MareAPI/MareSynchronosAPI/Dto/User/UserPairDto.cs new file mode 100644 index 0000000..3d92ad6 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/User/UserPairDto.cs @@ -0,0 +1,12 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MessagePack; + +namespace MareSynchronos.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record UserPairDto(UserData User, UserPermissions OwnPermissions, UserPermissions OtherPermissions) : UserDto(User) +{ + public UserPermissions OwnPermissions { get; set; } = OwnPermissions; + public UserPermissions OtherPermissions { get; set; } = OtherPermissions; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/User/UserPermissionsDto.cs b/MareAPI/MareSynchronosAPI/Dto/User/UserPermissionsDto.cs new file mode 100644 index 0000000..772040b --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/User/UserPermissionsDto.cs @@ -0,0 +1,8 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MessagePack; + +namespace MareSynchronos.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record UserPermissionsDto(UserData User, UserPermissions Permissions) : UserDto(User); \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/User/UserProfileDto.cs b/MareAPI/MareSynchronosAPI/Dto/User/UserProfileDto.cs new file mode 100644 index 0000000..0b103e5 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/User/UserProfileDto.cs @@ -0,0 +1,7 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record UserProfileDto(UserData User, bool Disabled, bool? IsNSFW, string? ProfilePictureBase64, string? Description) : UserDto(User); \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/User/UserProfileReportDto.cs b/MareAPI/MareSynchronosAPI/Dto/User/UserProfileReportDto.cs new file mode 100644 index 0000000..02ed9ef --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/User/UserProfileReportDto.cs @@ -0,0 +1,7 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record UserProfileReportDto(UserData User, string ProfileReport) : UserDto(User); \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/MareSynchronos.API.csproj b/MareAPI/MareSynchronosAPI/MareSynchronos.API.csproj new file mode 100644 index 0000000..44e5fc8 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/MareSynchronos.API.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/MareAPI/MareSynchronosAPI/MareSynchronosAPI.sln b/MareAPI/MareSynchronosAPI/MareSynchronosAPI.sln new file mode 100644 index 0000000..ffde134 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/MareSynchronosAPI.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32602.215 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronos.API", "MareSynchronos.API.csproj", "{CD05EE19-802F-4490-AAD8-CAD4BF1D630D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CD05EE19-802F-4490-AAD8-CAD4BF1D630D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD05EE19-802F-4490-AAD8-CAD4BF1D630D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD05EE19-802F-4490-AAD8-CAD4BF1D630D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD05EE19-802F-4490-AAD8-CAD4BF1D630D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DFB70C71-AB27-468D-A08B-218CA79BF69D} + EndGlobalSection +EndGlobal diff --git a/MareAPI/MareSynchronosAPI/Routes/MareAuth.cs b/MareAPI/MareSynchronosAPI/Routes/MareAuth.cs new file mode 100644 index 0000000..2bef31e --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Routes/MareAuth.cs @@ -0,0 +1,14 @@ +namespace MareSynchronos.API.Routes; + +public class MareAuth +{ + public const string Auth = "/auth"; + public const string Auth_CreateIdent = "createWithIdent"; + public const string Auth_CreateIdentV2 = "createWithIdentV2"; + public const string Auth_Register = "registerNewKey"; + public const string Auth_RegisterV2 = "registerNewKeyV2"; + public static Uri AuthFullPath(Uri baseUri) => new Uri(baseUri, Auth + "/" + Auth_CreateIdent); + public static Uri AuthV2FullPath(Uri baseUri) => new Uri(baseUri, Auth + "/" + Auth_CreateIdentV2); + public static Uri AuthRegisterFullPath(Uri baseUri) => new Uri(baseUri, Auth + "/" + Auth_Register); + public static Uri AuthRegisterV2FullPath(Uri baseUri) => new Uri(baseUri, Auth + "/" + Auth_RegisterV2); +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Routes/MareFiles.cs b/MareAPI/MareSynchronosAPI/Routes/MareFiles.cs new file mode 100644 index 0000000..a4e5f5d --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Routes/MareFiles.cs @@ -0,0 +1,45 @@ +namespace MareSynchronos.API.Routes; + +public class MareFiles +{ + public const string Cache = "/cache"; + public const string Cache_Get = "get"; + + public const string Request = "/request"; + public const string Request_Cancel = "cancel"; + public const string Request_Check = "check"; + public const string Request_Enqueue = "enqueue"; + public const string Request_RequestFile = "file"; + + public const string ServerFiles = "/files"; + public const string ServerFiles_DeleteAll = "deleteAll"; + public const string ServerFiles_FilesSend = "filesSend"; + public const string ServerFiles_GetSizes = "getFileSizes"; + public const string ServerFiles_Upload = "upload"; + public const string ServerFiles_UploadRaw = "uploadRaw"; + public const string ServerFiles_UploadMunged = "uploadMunged"; + + public const string Distribution = "/dist"; + public const string Distribution_Get = "get"; + + public const string Main = "/main"; + public const string Main_SendReady = "sendReady"; + + public static Uri CacheGetFullPath(Uri baseUri, Guid requestId) => new(baseUri, Cache + "/" + Cache_Get + "?requestId=" + requestId.ToString()); + + public static Uri RequestCancelFullPath(Uri baseUri, Guid guid) => new Uri(baseUri, Request + "/" + Request_Cancel + "?requestId=" + guid.ToString()); + public static Uri RequestCheckQueueFullPath(Uri baseUri, Guid guid) => new Uri(baseUri, Request + "/" + Request_Check + "?requestId=" + guid.ToString()); + public static Uri RequestEnqueueFullPath(Uri baseUri) => new(baseUri, Request + "/" + Request_Enqueue); + public static Uri RequestRequestFileFullPath(Uri baseUri, string hash) => new(baseUri, Request + "/" + Request_RequestFile + "?file=" + hash); + + public static Uri ServerFilesDeleteAllFullPath(Uri baseUri) => new(baseUri, ServerFiles + "/" + ServerFiles_DeleteAll); + public static Uri ServerFilesFilesSendFullPath(Uri baseUri) => new(baseUri, ServerFiles + "/" + ServerFiles_FilesSend); + public static Uri ServerFilesGetSizesFullPath(Uri baseUri) => new(baseUri, ServerFiles + "/" + ServerFiles_GetSizes); + public static Uri ServerFilesUploadFullPath(Uri baseUri, string hash) => new(baseUri, ServerFiles + "/" + ServerFiles_Upload + "/" + hash); + public static Uri ServerFilesUploadRawFullPath(Uri baseUri, string hash) => new(baseUri, ServerFiles + "/" + ServerFiles_UploadRaw + "/" + hash); + public static Uri ServerFilesUploadMunged(Uri baseUri, string hash) => new(baseUri, ServerFiles + "/" + ServerFiles_UploadMunged + "/" + hash); + + public static Uri DistributionGetFullPath(Uri baseUri, string hash) => new(baseUri, Distribution + "/" + Distribution_Get + "?file=" + hash); + + public static Uri MainSendReadyFullPath(Uri baseUri, string uid, Guid request) => new(baseUri, Main + "/" + Main_SendReady + "/" + "?uid=" + uid + "&requestId=" + request.ToString()); +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/SignalR/IMareHub.cs b/MareAPI/MareSynchronosAPI/SignalR/IMareHub.cs new file mode 100644 index 0000000..7475116 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/SignalR/IMareHub.cs @@ -0,0 +1,144 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Dto; +using MareSynchronos.API.Dto.CharaData; +using MareSynchronos.API.Dto.Chat; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.API.Dto.User; + +namespace MareSynchronos.API.SignalR; + +public interface IMareHub +{ + const int ApiVersion = 1026; + const string Path = "/mare"; + + Task CheckClientHealth(); + + Task Client_DownloadReady(Guid requestId); + + Task Client_GroupChangePermissions(GroupPermissionDto groupPermission); + + Task Client_GroupChatMsg(GroupChatMsgDto groupChatMsgDto); + + Task Client_GroupDelete(GroupDto groupDto); + + Task Client_GroupPairChangePermissions(GroupPairUserPermissionDto permissionDto); + + Task Client_GroupPairChangeUserInfo(GroupPairUserInfoDto userInfo); + + Task Client_GroupPairJoined(GroupPairFullInfoDto groupPairInfoDto); + + Task Client_GroupPairLeft(GroupPairDto groupPairDto); + + Task Client_GroupSendFullInfo(GroupFullInfoDto groupInfo); + + Task Client_GroupSendInfo(GroupInfoDto groupInfo); + + Task Client_ReceiveServerMessage(MessageSeverity messageSeverity, string message); + + Task Client_UpdateSystemInfo(SystemInfoDto systemInfo); + + Task Client_UserAddClientPair(UserPairDto dto); + + Task Client_UserChatMsg(UserChatMsgDto chatMsgDto); + + Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dataDto); + + Task Client_UserReceiveUploadStatus(UserDto dto); + + Task Client_UserRemoveClientPair(UserDto dto); + + Task Client_UserSendOffline(UserDto dto); + + Task Client_UserSendOnline(OnlineUserIdentDto dto); + + Task Client_UserUpdateOtherPairPermissions(UserPermissionsDto dto); + + Task Client_UserUpdateProfile(UserDto dto); + + Task Client_UserUpdateSelfPairPermissions(UserPermissionsDto dto); + + Task Client_GposeLobbyJoin(UserData userData); + Task Client_GposeLobbyLeave(UserData userData); + Task Client_GposeLobbyPushCharacterData(CharaDataDownloadDto charaDownloadDto); + Task Client_GposeLobbyPushPoseData(UserData userData, PoseData poseData); + Task Client_GposeLobbyPushWorldData(UserData userData, WorldData worldData); + + Task GetConnectionDto(); + + Task GroupBanUser(GroupPairDto dto, string reason); + + Task GroupChangeGroupPermissionState(GroupPermissionDto dto); + + Task GroupChangeIndividualPermissionState(GroupPairUserPermissionDto dto); + + Task GroupChangeOwnership(GroupPairDto groupPair); + + Task GroupChangePassword(GroupPasswordDto groupPassword); + + Task GroupChatSendMsg(GroupDto group, ChatMessage message); + + Task GroupClear(GroupDto group); + + Task GroupCreate(); + + Task> GroupCreateTempInvite(GroupDto group, int amount); + + Task GroupDelete(GroupDto group); + + Task> GroupGetBannedUsers(GroupDto group); + + Task GroupJoin(GroupPasswordDto passwordedGroup); + + Task GroupLeave(GroupDto group); + + Task GroupRemoveUser(GroupPairDto groupPair); + + Task GroupSetUserInfo(GroupPairUserInfoDto groupPair); + + Task> GroupsGetAll(); + + Task> GroupsGetUsersInGroup(GroupDto group); + + Task GroupUnbanUser(GroupPairDto groupPair); + Task GroupPrune(GroupDto group, int days, bool execute); + + Task UserAddPair(UserDto user); + + Task UserChatSendMsg(UserDto user, ChatMessage message); + + Task UserDelete(); + + Task> UserGetOnlinePairs(); + + Task> UserGetPairedClients(); + + Task UserGetProfile(UserDto dto); + + Task UserPushData(UserCharaDataMessageDto dto); + + Task UserRemovePair(UserDto userDto); + + Task UserReportProfile(UserProfileReportDto userDto); + + Task UserSetPairPermissions(UserPermissionsDto userPermissions); + + Task UserSetProfile(UserProfileDto userDescription); + + Task CharaDataCreate(); + Task CharaDataUpdate(CharaDataUpdateDto updateDto); + Task CharaDataDelete(string id); + Task CharaDataGetMetainfo(string id); + Task CharaDataDownload(string id); + Task> CharaDataGetOwn(); + Task> CharaDataGetShared(); + Task CharaDataAttemptRestore(string id); + + Task GposeLobbyCreate(); + Task> GposeLobbyJoin(string lobbyId); + Task GposeLobbyLeave(); + Task GposeLobbyPushCharacterData(CharaDataDownloadDto charaDownloadDto); + Task GposeLobbyPushPoseData(PoseData poseData); + Task GposeLobbyPushWorldData(WorldData worldData); +} diff --git a/MareAPI/MareSynchronosAPI/SignalR/IMareHubClient.cs b/MareAPI/MareSynchronosAPI/SignalR/IMareHubClient.cs new file mode 100644 index 0000000..d13cbaa --- /dev/null +++ b/MareAPI/MareSynchronosAPI/SignalR/IMareHubClient.cs @@ -0,0 +1,62 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Dto; +using MareSynchronos.API.Dto.CharaData; +using MareSynchronos.API.Dto.Chat; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.API.Dto.User; + +namespace MareSynchronos.API.SignalR; + +public interface IMareHubClient : IMareHub +{ + void OnDownloadReady(Action act); + + void OnGroupChangePermissions(Action act); + + void OnGroupChatMsg(Action groupChatMsgDto); + + void OnGroupDelete(Action act); + + void OnGroupPairChangePermissions(Action act); + + void OnGroupPairChangeUserInfo(Action act); + + void OnGroupPairJoined(Action act); + + void OnGroupPairLeft(Action act); + + void OnGroupSendFullInfo(Action act); + + void OnGroupSendInfo(Action act); + + void OnReceiveServerMessage(Action act); + + void OnUpdateSystemInfo(Action act); + + void OnUserAddClientPair(Action act); + + void OnUserChatMsg(Action chatMsgDto); + + void OnUserReceiveCharacterData(Action act); + + void OnUserReceiveUploadStatus(Action act); + + void OnUserRemoveClientPair(Action act); + + void OnUserSendOffline(Action act); + + void OnUserSendOnline(Action act); + + void OnUserUpdateOtherPairPermissions(Action act); + + void OnUserUpdateProfile(Action act); + + void OnUserUpdateSelfPairPermissions(Action act); + + void OnGposeLobbyJoin(Action act); + void OnGposeLobbyLeave(Action act); + void OnGposeLobbyPushCharacterData(Action act); + void OnGposeLobbyPushPoseData(Action act); + void OnGposeLobbyPushWorldData(Action act); +} \ No newline at end of file diff --git a/MareSynchronosServer/.editorconfig b/MareSynchronosServer/.editorconfig new file mode 100644 index 0000000..6cf5454 --- /dev/null +++ b/MareSynchronosServer/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# MA0048: File name must match type name +dotnet_diagnostic.MA0048.severity = suggestion diff --git a/MareSynchronosServer/MareSynchronosAuthService/Authentication/SecretKeyAuthReply.cs b/MareSynchronosServer/MareSynchronosAuthService/Authentication/SecretKeyAuthReply.cs new file mode 100644 index 0000000..f6ef7cf --- /dev/null +++ b/MareSynchronosServer/MareSynchronosAuthService/Authentication/SecretKeyAuthReply.cs @@ -0,0 +1,3 @@ +namespace MareSynchronosAuthService.Authentication; + +public record SecretKeyAuthReply(bool Success, string Uid, string Alias, bool TempBan, bool Permaban); diff --git a/MareSynchronosServer/MareSynchronosAuthService/Authentication/SecretKeyFailedAuthorization.cs b/MareSynchronosServer/MareSynchronosAuthService/Authentication/SecretKeyFailedAuthorization.cs new file mode 100644 index 0000000..fcf6581 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosAuthService/Authentication/SecretKeyFailedAuthorization.cs @@ -0,0 +1,12 @@ +namespace MareSynchronosAuthService.Authentication; + +internal record SecretKeyFailedAuthorization +{ + private int failedAttempts = 1; + public int FailedAttempts => failedAttempts; + public Task ResetTask { get; set; } + public void IncreaseFailedAttempts() + { + Interlocked.Increment(ref failedAttempts); + } +} diff --git a/MareSynchronosServer/MareSynchronosAuthService/Controllers/JwtController.cs b/MareSynchronosServer/MareSynchronosAuthService/Controllers/JwtController.cs new file mode 100644 index 0000000..11323db --- /dev/null +++ b/MareSynchronosServer/MareSynchronosAuthService/Controllers/JwtController.cs @@ -0,0 +1,193 @@ +using MareSynchronos.API.Dto; +using MareSynchronos.API.Dto.Account; +using MareSynchronos.API.Routes; +using MareSynchronosAuthService.Services; +using MareSynchronosShared; +using MareSynchronosShared.Data; +using MareSynchronosShared.Models; +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils; +using MareSynchronosShared.Utils.Configuration; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using StackExchange.Redis.Extensions.Core.Abstractions; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace MareSynchronosAuthService.Controllers; + +[AllowAnonymous] +[Route(MareAuth.Auth)] +public class JwtController : Controller +{ + private readonly IHttpContextAccessor _accessor; + private readonly IRedisDatabase _redis; + private readonly IDbContextFactory _mareDbContextFactory; + private readonly GeoIPService _geoIPProvider; + private readonly SecretKeyAuthenticatorService _secretKeyAuthenticatorService; + private readonly AccountRegistrationService _accountRegistrationService; + private readonly IConfigurationService _configuration; + + public JwtController(ILogger logger, + IHttpContextAccessor accessor, IDbContextFactory mareDbContextFactory, + SecretKeyAuthenticatorService secretKeyAuthenticatorService, + AccountRegistrationService accountRegistrationService, + IConfigurationService configuration, + IRedisDatabase redisDb, GeoIPService geoIPProvider) + { + _accessor = accessor; + _redis = redisDb; + _geoIPProvider = geoIPProvider; + _mareDbContextFactory = mareDbContextFactory; + _secretKeyAuthenticatorService = secretKeyAuthenticatorService; + _accountRegistrationService = accountRegistrationService; + _configuration = configuration; + } + + [AllowAnonymous] + [HttpPost(MareAuth.Auth_CreateIdent)] + public async Task CreateToken(string auth, string charaIdent) + { + if (string.IsNullOrEmpty(auth)) return BadRequest("No Authkey"); + if (string.IsNullOrEmpty(charaIdent)) return BadRequest("No CharaIdent"); + + using var dbContext = await _mareDbContextFactory.CreateDbContextAsync(); + var ip = _accessor.GetIpAddress(); + + var authResult = await _secretKeyAuthenticatorService.AuthorizeAsync(ip, auth); + + var isBanned = await dbContext.BannedUsers.AsNoTracking().AnyAsync(u => u.CharacterIdentification == charaIdent).ConfigureAwait(false); + if (isBanned) + { + var authToBan = dbContext.Auth.SingleOrDefault(a => a.UserUID == authResult.Uid); + if (authToBan != null) + { + authToBan.IsBanned = true; + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + + return Unauthorized("Your character is banned from using the service."); + } + + if (!authResult.Success && !authResult.TempBan) return Unauthorized("The provided secret key is invalid. Verify your accounts existence and/or recover the secret key."); + if (!authResult.Success && authResult.TempBan) return Unauthorized("Due to an excessive amount of failed authentication attempts you are temporarily banned. Check your Secret Key configuration and try connecting again in 5 minutes."); + if (authResult.Permaban) + { + if (!dbContext.BannedUsers.Any(c => c.CharacterIdentification == charaIdent)) + { + dbContext.BannedUsers.Add(new Banned() + { + CharacterIdentification = charaIdent, + Reason = "Autobanned CharacterIdent (" + authResult.Uid + ")", + }); + + await dbContext.SaveChangesAsync(); + } + + var lodestone = await dbContext.LodeStoneAuth.Include(a => a.User).FirstOrDefaultAsync(c => c.User.UID == authResult.Uid); + + if (lodestone != null) + { + if (!dbContext.BannedRegistrations.Any(c => c.DiscordIdOrLodestoneAuth == lodestone.HashedLodestoneId)) + { + dbContext.BannedRegistrations.Add(new BannedRegistrations() + { + DiscordIdOrLodestoneAuth = lodestone.HashedLodestoneId, + }); + } + if (!dbContext.BannedRegistrations.Any(c => c.DiscordIdOrLodestoneAuth == lodestone.DiscordId.ToString())) + { + dbContext.BannedRegistrations.Add(new BannedRegistrations() + { + DiscordIdOrLodestoneAuth = lodestone.DiscordId.ToString(), + }); + } + + await dbContext.SaveChangesAsync(); + } + + return Unauthorized("You are permanently banned."); + } + + var existingIdent = await _redis.GetAsync("UID:" + authResult.Uid); + if (!string.IsNullOrEmpty(existingIdent)) return Unauthorized("Already logged in to this account. Reconnect in 60 seconds. If you keep seeing this issue, restart your game."); + + var token = CreateToken(new List() + { + new Claim(MareClaimTypes.Uid, authResult.Uid), + new Claim(MareClaimTypes.CharaIdent, charaIdent), + new Claim(MareClaimTypes.Alias, authResult.Alias), + new Claim(MareClaimTypes.Continent, await _geoIPProvider.GetCountryFromIP(_accessor)), + }); + + return Content(token.RawData); + } + + [AllowAnonymous] + [HttpPost(MareAuth.Auth_CreateIdentV2)] + public async Task CreateTokenV2(string auth, string charaIdent) + { + var tokenResponse = await CreateToken(auth, charaIdent); + var tokenContent = tokenResponse as ContentResult; + if (tokenContent == null) + return tokenResponse; + return Json(new AuthReplyDto + { + Token = tokenContent.Content, + WellKnown = _configuration.GetValueOrDefault(nameof(AuthServiceConfiguration.WellKnown), string.Empty), + }); + } + + [AllowAnonymous] + [HttpPost(MareAuth.Auth_Register)] + public async Task Register() + { + var ua = HttpContext.Request.Headers["User-Agent"][0] ?? "-"; + var ip = _accessor.GetIpAddress(); + + // Legacy endpoint: generate a secret key for the user + var computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString()); + var hashedKey = StringUtils.Sha256String(computedHash); + + var dto = await _accountRegistrationService.RegisterAccountAsync(ua, ip, hashedKey); + + return Json(new RegisterReplyDto() + { + Success = dto.Success, + ErrorMessage = dto.ErrorMessage, + UID = dto.UID, + SecretKey = computedHash + }); + } + + [AllowAnonymous] + [HttpPost(MareAuth.Auth_RegisterV2)] + public async Task RegisterV2(string hashedSecretKey) + { + if (string.IsNullOrEmpty(hashedSecretKey)) return BadRequest("No HashedSecretKey"); + if (hashedSecretKey.Length != 64) return BadRequest("Bad HashedSecretKey"); + if (!hashedSecretKey.All(char.IsAsciiHexDigitUpper)) return BadRequest("Bad HashedSecretKey"); + + var ua = HttpContext.Request.Headers["User-Agent"][0] ?? "-"; + var ip = _accessor.GetIpAddress(); + return Json(await _accountRegistrationService.RegisterAccountAsync(ua, ip, hashedSecretKey)); + } + + private JwtSecurityToken CreateToken(IEnumerable authClaims) + { + var authSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_configuration.GetValue(nameof(MareConfigurationBase.Jwt)))); + + var token = new SecurityTokenDescriptor() + { + Subject = new ClaimsIdentity(authClaims), + SigningCredentials = new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256Signature), + }; + + var handler = new JwtSecurityTokenHandler(); + return handler.CreateJwtSecurityToken(token); + } +} + diff --git a/MareSynchronosServer/MareSynchronosAuthService/MareSynchronosAuthService.csproj b/MareSynchronosServer/MareSynchronosAuthService/MareSynchronosAuthService.csproj new file mode 100644 index 0000000..fcb62be --- /dev/null +++ b/MareSynchronosServer/MareSynchronosAuthService/MareSynchronosAuthService.csproj @@ -0,0 +1,38 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + Never + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/MareSynchronosServer/MareSynchronosAuthService/Program.cs b/MareSynchronosServer/MareSynchronosAuthService/Program.cs new file mode 100644 index 0000000..5bebfd8 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosAuthService/Program.cs @@ -0,0 +1,40 @@ +namespace MareSynchronosAuthService; + +public class Program +{ + public static void Main(string[] args) + { + var hostBuilder = CreateHostBuilder(args); + using var host = hostBuilder.Build(); + try + { + host.Run(); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.ClearProviders(); + builder.AddConsole(); + }); + var logger = loggerFactory.CreateLogger(); + return Host.CreateDefaultBuilder(args) + .UseSystemd() + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseContentRoot(AppContext.BaseDirectory); + webBuilder.ConfigureLogging((ctx, builder) => + { + builder.AddConfiguration(ctx.Configuration.GetSection("Logging")); + builder.AddFile(o => o.RootPath = AppContext.BaseDirectory); + }); + webBuilder.UseStartup(ctx => new Startup(ctx.Configuration, logger)); + }); + } +} diff --git a/MareSynchronosServer/MareSynchronosAuthService/Properties/launchSettings.json b/MareSynchronosServer/MareSynchronosAuthService/Properties/launchSettings.json new file mode 100644 index 0000000..db41dc1 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosAuthService/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:37726", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5056", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/MareSynchronosServer/MareSynchronosAuthService/Services/AccountRegistrationService.cs b/MareSynchronosServer/MareSynchronosAuthService/Services/AccountRegistrationService.cs new file mode 100644 index 0000000..2b0a1b0 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosAuthService/Services/AccountRegistrationService.cs @@ -0,0 +1,152 @@ +using System.Collections.Concurrent; +using MareSynchronos.API.Dto.Account; +using MareSynchronosShared.Data; +using MareSynchronosShared.Metrics; +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils; +using MareSynchronosShared.Utils.Configuration; +using Microsoft.EntityFrameworkCore; +using System.Text.RegularExpressions; +using MareSynchronosShared.Models; + +namespace MareSynchronosAuthService.Services; + +internal record IpRegistrationCount +{ + private int count = 1; + public int Count => count; + public Task ResetTask { get; set; } + public CancellationTokenSource ResetTaskCts { get; set; } + public void IncreaseCount() + { + Interlocked.Increment(ref count); + } +} + +public class AccountRegistrationService +{ + private readonly MareMetrics _metrics; + private readonly MareDbContext _mareDbContext; + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly IConfigurationService _configurationService; + private readonly ILogger _logger; + private readonly ConcurrentDictionary _registrationsPerIp = new(StringComparer.Ordinal); + + private Regex _registrationUserAgentRegex = new Regex(@"^MareSynchronos/", RegexOptions.Compiled); + + public AccountRegistrationService(MareMetrics metrics, MareDbContext mareDbContext, + IServiceScopeFactory serviceScopeFactory, IConfigurationService configuration, + ILogger logger) + { + _mareDbContext = mareDbContext; + _logger = logger; + _configurationService = configuration; + _metrics = metrics; + _serviceScopeFactory = serviceScopeFactory; + } + + public async Task RegisterAccountAsync(string ua, string ip, string hashedSecretKey) + { + var reply = new RegisterReplyV2Dto(); + + if (!_registrationUserAgentRegex.Match(ua).Success) + { + reply.ErrorMessage = "User-Agent not allowed"; + return reply; + } + + if (_registrationsPerIp.TryGetValue(ip, out var registrationCount) + && registrationCount.Count >= _configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.RegisterIpLimit), 3)) + { + _logger.LogWarning("Rejecting {ip} for registration spam", ip); + + if (registrationCount.ResetTask == null) + { + registrationCount.ResetTaskCts = new CancellationTokenSource(); + + if (registrationCount.ResetTaskCts != null) + registrationCount.ResetTaskCts.Cancel(); + + registrationCount.ResetTask = Task.Run(async () => + { + await Task.Delay(TimeSpan.FromMinutes(_configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.RegisterIpDurationInMinutes), 10))).ConfigureAwait(false); + + }).ContinueWith((t) => + { + _registrationsPerIp.Remove(ip, out _); + }, registrationCount.ResetTaskCts.Token); + } + reply.ErrorMessage = "Too many registrations from this IP. Please try again later."; + return reply; + } + + var user = new User(); + + var hasValidUid = false; + while (!hasValidUid) + { + var uid = StringUtils.GenerateRandomString(7); + if (_mareDbContext.Users.Any(u => u.UID == uid || u.Alias == uid)) continue; + user.UID = uid; + hasValidUid = true; + } + + // make the first registered user on the service to admin + if (!await _mareDbContext.Users.AnyAsync().ConfigureAwait(false)) + { + user.IsAdmin = true; + } + + user.LastLoggedIn = DateTime.UtcNow; + + var auth = new Auth() + { + HashedKey = hashedSecretKey, + User = user, + }; + + await _mareDbContext.Users.AddAsync(user).ConfigureAwait(false); + await _mareDbContext.Auth.AddAsync(auth).ConfigureAwait(false); + await _mareDbContext.SaveChangesAsync().ConfigureAwait(false); + + _logger.LogInformation("User registered: {userUID} from IP {ip}", user.UID, ip); + _metrics.IncCounter(MetricsAPI.CounterAccountsCreated); + + reply.Success = true; + reply.UID = user.UID; + + RecordIpRegistration(ip); + + return reply; + } + + private void RecordIpRegistration(string ip) + { + var whitelisted = _configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.WhitelistedIps), new List()); + if (!whitelisted.Any(w => ip.Contains(w, StringComparison.OrdinalIgnoreCase))) + { + if (_registrationsPerIp.TryGetValue(ip, out var count)) + { + count.IncreaseCount(); + } + else + { + count = _registrationsPerIp[ip] = new IpRegistrationCount(); + + if (count.ResetTaskCts != null) + count.ResetTaskCts.Cancel(); + + count.ResetTaskCts = new CancellationTokenSource(); + + count.ResetTask = Task.Run(async () => + { + await Task.Delay(TimeSpan.FromMinutes(_configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.RegisterIpDurationInMinutes), 10))).ConfigureAwait(false); + + }).ContinueWith((t) => + { + _registrationsPerIp.Remove(ip, out _); + }, count.ResetTaskCts.Token); + } + } + } +} diff --git a/MareSynchronosServer/MareSynchronosAuthService/Services/GeoIPService.cs b/MareSynchronosServer/MareSynchronosAuthService/Services/GeoIPService.cs new file mode 100644 index 0000000..dbebf35 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosAuthService/Services/GeoIPService.cs @@ -0,0 +1,138 @@ +using MareSynchronosShared; +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils.Configuration; +using MaxMind.GeoIP2; + +namespace MareSynchronosAuthService.Services; + +public class GeoIPService : IHostedService +{ + private readonly ILogger _logger; + private readonly IConfigurationService _mareConfiguration; + private bool _useGeoIP = false; + private string _cityFile = string.Empty; + private DatabaseReader? _dbReader; + private DateTime _dbLastWriteTime = DateTime.Now; + private CancellationTokenSource _fileWriteTimeCheckCts = new(); + private bool _processingReload = false; + + public GeoIPService(ILogger logger, + IConfigurationService mareConfiguration) + { + _logger = logger; + _mareConfiguration = mareConfiguration; + } + + public async Task GetCountryFromIP(IHttpContextAccessor httpContextAccessor) + { + if (!_useGeoIP) + { + return "*"; + } + + try + { + var ip = httpContextAccessor.GetIpAddress(); + + using CancellationTokenSource waitCts = new(); + waitCts.CancelAfter(TimeSpan.FromSeconds(5)); + while (_processingReload) await Task.Delay(100, waitCts.Token).ConfigureAwait(false); + + if (_dbReader!.TryCity(ip, out var response)) + { + string? continent = response?.Continent.Code; + if (!string.IsNullOrEmpty(continent) && + string.Equals(continent, "NA", StringComparison.Ordinal) + && response?.Location.Longitude != null) + { + if (response.Location.Longitude < -102) + { + continent = "NA-W"; + } + else + { + continent = "NA-E"; + } + } + + return continent ?? "*"; + } + + return "*"; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error handling Geo IP country in request"); + return "*"; + } + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("GeoIP module starting update task"); + + var token = _fileWriteTimeCheckCts.Token; + _ = PeriodicReloadTask(token); + + return Task.CompletedTask; + } + + private async Task PeriodicReloadTask(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + try + { + _processingReload = true; + + var useGeoIP = _mareConfiguration.GetValueOrDefault(nameof(AuthServiceConfiguration.UseGeoIP), false); + var cityFile = _mareConfiguration.GetValueOrDefault(nameof(AuthServiceConfiguration.GeoIPDbCityFile), string.Empty); + var lastWriteTime = new FileInfo(cityFile).LastWriteTimeUtc; + if (useGeoIP && (!string.Equals(cityFile, _cityFile, StringComparison.OrdinalIgnoreCase) || lastWriteTime != _dbLastWriteTime)) + { + _cityFile = cityFile; + if (!File.Exists(_cityFile)) throw new FileNotFoundException($"Could not open GeoIP City Database, path does not exist: {_cityFile}"); + _dbReader?.Dispose(); + _dbReader = null; + _dbReader = new DatabaseReader(_cityFile); + _dbLastWriteTime = lastWriteTime; + + _ = _dbReader.City("8.8.8.8").Continent; + + _logger.LogInformation($"Loaded GeoIP city file from {_cityFile}"); + + if (_useGeoIP != useGeoIP) + { + _logger.LogInformation("GeoIP module is now enabled"); + _useGeoIP = useGeoIP; + } + } + + if (_useGeoIP != useGeoIP && !useGeoIP) + { + _logger.LogInformation("GeoIP module is now disabled"); + _useGeoIP = useGeoIP; + } + } + catch (Exception e) + { + _logger.LogWarning(e, "Error during periodic GeoIP module reload task, disabling GeoIP"); + _useGeoIP = false; + } + finally + { + _processingReload = false; + } + + await Task.Delay(TimeSpan.FromMinutes(1)).ConfigureAwait(false); + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _fileWriteTimeCheckCts.Cancel(); + _fileWriteTimeCheckCts.Dispose(); + _dbReader?.Dispose(); + return Task.CompletedTask; + } +} diff --git a/MareSynchronosServer/MareSynchronosAuthService/Services/SecretKeyAuthenticatorService.cs b/MareSynchronosServer/MareSynchronosAuthService/Services/SecretKeyAuthenticatorService.cs new file mode 100644 index 0000000..bf246f2 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosAuthService/Services/SecretKeyAuthenticatorService.cs @@ -0,0 +1,88 @@ +using System.Collections.Concurrent; +using MareSynchronosAuthService.Authentication; +using MareSynchronosShared.Data; +using MareSynchronosShared.Metrics; +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils.Configuration; +using Microsoft.EntityFrameworkCore; + +namespace MareSynchronosAuthService.Services; + +public class SecretKeyAuthenticatorService +{ + private readonly MareMetrics _metrics; + private readonly MareDbContext _mareDbContext; + private readonly IConfigurationService _configurationService; + private readonly ILogger _logger; + private readonly ConcurrentDictionary _failedAuthorizations = new(StringComparer.Ordinal); + + public SecretKeyAuthenticatorService(MareMetrics metrics, MareDbContext mareDbContext, + IConfigurationService configuration, ILogger logger) + { + _logger = logger; + _configurationService = configuration; + _metrics = metrics; + _mareDbContext = mareDbContext; + } + + public async Task AuthorizeAsync(string ip, string hashedSecretKey) + { + _metrics.IncCounter(MetricsAPI.CounterAuthenticationRequests); + + if (_failedAuthorizations.TryGetValue(ip, out var existingFailedAuthorization) + && existingFailedAuthorization.FailedAttempts > _configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.FailedAuthForTempBan), 5)) + { + if (existingFailedAuthorization.ResetTask == null) + { + _logger.LogWarning("TempBan {ip} for authorization spam", ip); + + existingFailedAuthorization.ResetTask = Task.Run(async () => + { + await Task.Delay(TimeSpan.FromMinutes(_configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.TempBanDurationInMinutes), 5))).ConfigureAwait(false); + + }).ContinueWith((t) => + { + _failedAuthorizations.Remove(ip, out _); + }); + } + return new(Success: false, Uid: null, TempBan: true, Alias: null, Permaban: false); + } + + var authReply = await _mareDbContext.Auth.Include(a => a.User).AsNoTracking() + .SingleOrDefaultAsync(u => u.HashedKey == hashedSecretKey).ConfigureAwait(false); + + SecretKeyAuthReply reply = new(authReply != null, authReply?.UserUID, authReply?.User?.Alias ?? string.Empty, TempBan: false, authReply?.IsBanned ?? false); + + if (reply.Success) + { + _metrics.IncCounter(MetricsAPI.CounterAuthenticationSuccesses); + } + else + { + return AuthenticationFailure(ip); + } + + return reply; + } + + private SecretKeyAuthReply AuthenticationFailure(string ip) + { + _metrics.IncCounter(MetricsAPI.CounterAuthenticationFailures); + + _logger.LogWarning("Failed authorization from {ip}", ip); + var whitelisted = _configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.WhitelistedIps), new List()); + if (!whitelisted.Any(w => ip.Contains(w, StringComparison.OrdinalIgnoreCase))) + { + if (_failedAuthorizations.TryGetValue(ip, out var auth)) + { + auth.IncreaseFailedAttempts(); + } + else + { + _failedAuthorizations[ip] = new SecretKeyFailedAuthorization(); + } + } + + return new(Success: false, Uid: null, Alias: null, TempBan: false, Permaban: false); + } +} diff --git a/MareSynchronosServer/MareSynchronosAuthService/Startup.cs b/MareSynchronosServer/MareSynchronosAuthService/Startup.cs new file mode 100644 index 0000000..bd06ef5 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosAuthService/Startup.cs @@ -0,0 +1,226 @@ +using MareSynchronosAuthService.Controllers; +using MareSynchronosShared.Metrics; +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils; +using Microsoft.AspNetCore.Mvc.Controllers; +using StackExchange.Redis.Extensions.Core.Configuration; +using StackExchange.Redis.Extensions.System.Text.Json; +using StackExchange.Redis; +using System.Net; +using MareSynchronosAuthService.Services; +using MareSynchronosShared.RequirementHandlers; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using System.Text; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Prometheus; +using MareSynchronosShared.Utils.Configuration; + +namespace MareSynchronosAuthService; + +public class Startup +{ + private readonly IConfiguration _configuration; + private ILogger _logger; + + public Startup(IConfiguration configuration, ILogger logger) + { + _configuration = configuration; + _logger = logger; + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger logger) + { + var config = app.ApplicationServices.GetRequiredService>(); + + app.UseRouting(); + + app.UseHttpMetrics(); + + app.UseAuthentication(); + app.UseAuthorization(); + + KestrelMetricServer metricServer = new KestrelMetricServer(config.GetValueOrDefault(nameof(MareConfigurationBase.MetricsPort), 4985)); + metricServer.Start(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + + foreach (var source in endpoints.DataSources.SelectMany(e => e.Endpoints).Cast()) + { + if (source == null) continue; + _logger.LogInformation("Endpoint: {url} ", source.RoutePattern.RawText); + } + }); + } + + public void ConfigureServices(IServiceCollection services) + { + var mareConfig = _configuration.GetRequiredSection("MareSynchronos"); + + services.AddHttpContextAccessor(); + + ConfigureRedis(services, mareConfig); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddHostedService(provider => provider.GetRequiredService()); + + services.Configure(_configuration.GetRequiredSection("MareSynchronos")); + services.Configure(_configuration.GetRequiredSection("MareSynchronos")); + + services.AddSingleton(); + + ConfigureAuthorization(services); + + ConfigureDatabase(services, mareConfig); + + ConfigureConfigServices(services); + + ConfigureMetrics(services); + + services.AddHealthChecks(); + services.AddControllers().ConfigureApplicationPartManager(a => + { + a.FeatureProviders.Remove(a.FeatureProviders.OfType().First()); + a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(JwtController))); + }); + } + + private static void ConfigureAuthorization(IServiceCollection services) + { + services.AddTransient(); + + services.AddOptions(JwtBearerDefaults.AuthenticationScheme) + .Configure>((options, config) => + { + options.TokenValidationParameters = new() + { + ValidateIssuer = false, + ValidateLifetime = true, + ValidateAudience = false, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.GetValue(nameof(MareConfigurationBase.Jwt)))), + }; + }); + + services.AddAuthentication(o => + { + o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(); + + services.AddAuthorization(options => + { + options.DefaultPolicy = new AuthorizationPolicyBuilder() + .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme) + .RequireAuthenticatedUser().Build(); + options.AddPolicy("Authenticated", policy => + { + policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme); + policy.RequireAuthenticatedUser(); + }); + options.AddPolicy("Identified", policy => + { + policy.AddRequirements(new UserRequirement(UserRequirements.Identified)); + + }); + options.AddPolicy("Admin", policy => + { + policy.AddRequirements(new UserRequirement(UserRequirements.Identified | UserRequirements.Administrator)); + + }); + options.AddPolicy("Moderator", policy => + { + policy.AddRequirements(new UserRequirement(UserRequirements.Identified | UserRequirements.Moderator | UserRequirements.Administrator)); + }); + options.AddPolicy("Internal", new AuthorizationPolicyBuilder().RequireClaim(MareClaimTypes.Internal, "true").Build()); + }); + } + + private static void ConfigureMetrics(IServiceCollection services) + { + services.AddSingleton(m => new MareMetrics(m.GetService>(), new List + { + MetricsAPI.CounterAuthenticationCacheHits, + MetricsAPI.CounterAuthenticationFailures, + MetricsAPI.CounterAuthenticationRequests, + MetricsAPI.CounterAuthenticationSuccesses, + MetricsAPI.CounterAccountsCreated, + }, new List + { + })); + } + + private static void ConfigureRedis(IServiceCollection services, IConfigurationSection mareConfig) + { + // configure redis for SignalR + var redisConnection = mareConfig.GetValue(nameof(ServerConfiguration.RedisConnectionString), string.Empty); + + var options = ConfigurationOptions.Parse(redisConnection); + + var endpoint = options.EndPoints[0]; + string address = ""; + int port = 0; + if (endpoint is DnsEndPoint dnsEndPoint) { address = dnsEndPoint.Host; port = dnsEndPoint.Port; } + if (endpoint is IPEndPoint ipEndPoint) { address = ipEndPoint.Address.ToString(); port = ipEndPoint.Port; } + var redisConfiguration = new RedisConfiguration() + { + AbortOnConnectFail = true, + KeyPrefix = "", + Hosts = new RedisHost[] + { + new RedisHost(){ Host = address, Port = port }, + }, + AllowAdmin = true, + ConnectTimeout = options.ConnectTimeout, + Database = 0, + Ssl = false, + Password = options.Password, + ServerEnumerationStrategy = new ServerEnumerationStrategy() + { + Mode = ServerEnumerationStrategy.ModeOptions.All, + TargetRole = ServerEnumerationStrategy.TargetRoleOptions.Any, + UnreachableServerAction = ServerEnumerationStrategy.UnreachableServerActionOptions.Throw, + }, + MaxValueLength = 1024, + PoolSize = mareConfig.GetValue(nameof(ServerConfiguration.RedisPool), 50), + SyncTimeout = options.SyncTimeout, + }; + + services.AddStackExchangeRedisExtensions(redisConfiguration); + } + private void ConfigureConfigServices(IServiceCollection services) + { + services.AddSingleton, MareConfigurationServiceServer>(); + services.AddSingleton, MareConfigurationServiceServer>(); + } + + private void ConfigureDatabase(IServiceCollection services, IConfigurationSection mareConfig) + { + services.AddDbContextPool(options => + { + options.UseNpgsql(_configuration.GetConnectionString("DefaultConnection"), builder => + { + builder.MigrationsHistoryTable("_efmigrationshistory", "public"); + builder.MigrationsAssembly("MareSynchronosShared"); + }).UseSnakeCaseNamingConvention(); + options.EnableThreadSafetyChecks(false); + }, mareConfig.GetValue(nameof(MareConfigurationBase.DbContextPoolSize), 1024)); + services.AddDbContextFactory(options => + { + options.UseNpgsql(_configuration.GetConnectionString("DefaultConnection"), builder => + { + builder.MigrationsHistoryTable("_efmigrationshistory", "public"); + builder.MigrationsAssembly("MareSynchronosShared"); + }).UseSnakeCaseNamingConvention(); + options.EnableThreadSafetyChecks(false); + }); + } +} diff --git a/MareSynchronosServer/MareSynchronosAuthService/appsettings.Development.json b/MareSynchronosServer/MareSynchronosAuthService/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/MareSynchronosServer/MareSynchronosAuthService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/MareSynchronosServer/MareSynchronosAuthService/appsettings.json b/MareSynchronosServer/MareSynchronosAuthService/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosAuthService/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/MareSynchronosServer/MareSynchronosServer.sln b/MareSynchronosServer/MareSynchronosServer.sln new file mode 100644 index 0000000..84f8d6f --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer.sln @@ -0,0 +1,60 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32602.215 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronosServer", "MareSynchronosServer\MareSynchronosServer.csproj", "{029CA97F-E0BA-4172-A191-EA21FB61AD0F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronos.API", "..\MareAPI\MareSynchronosAPI\MareSynchronos.API.csproj", "{326BFB1B-5571-47A6-8513-1FFDB32D53B0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronosShared", "MareSynchronosShared\MareSynchronosShared.csproj", "{67B1461D-E215-4BA8-A64D-E1836724D5E6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronosStaticFilesServer", "MareSynchronosStaticFilesServer\MareSynchronosStaticFilesServer.csproj", "{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronosServices", "MareSynchronosServices\MareSynchronosServices.csproj", "{E29C8677-AB44-4950-9EB1-D8E70B710A56}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7D5C2B87-5CC9-4FE7-AD13-4C13F6600683}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MareSynchronosAuthService", "MareSynchronosAuthService\MareSynchronosAuthService.csproj", "{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Release|Any CPU.Build.0 = Release|Any CPU + {326BFB1B-5571-47A6-8513-1FFDB32D53B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {326BFB1B-5571-47A6-8513-1FFDB32D53B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {326BFB1B-5571-47A6-8513-1FFDB32D53B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {326BFB1B-5571-47A6-8513-1FFDB32D53B0}.Release|Any CPU.Build.0 = Release|Any CPU + {67B1461D-E215-4BA8-A64D-E1836724D5E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {67B1461D-E215-4BA8-A64D-E1836724D5E6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {67B1461D-E215-4BA8-A64D-E1836724D5E6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {67B1461D-E215-4BA8-A64D-E1836724D5E6}.Release|Any CPU.Build.0 = Release|Any CPU + {3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Release|Any CPU.Build.0 = Release|Any CPU + {E29C8677-AB44-4950-9EB1-D8E70B710A56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E29C8677-AB44-4950-9EB1-D8E70B710A56}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E29C8677-AB44-4950-9EB1-D8E70B710A56}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E29C8677-AB44-4950-9EB1-D8E70B710A56}.Release|Any CPU.Build.0 = Release|Any CPU + {D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {78C476A5-6E88-449B-828D-A2465D9D3295} + EndGlobalSection +EndGlobal diff --git a/MareSynchronosServer/MareSynchronosServer/.config/dotnet-tools.json b/MareSynchronosServer/MareSynchronosServer/.config/dotnet-tools.json new file mode 100644 index 0000000..b8c6c1f --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "6.0.9", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServer/Controllers/ClientMessageController.cs b/MareSynchronosServer/MareSynchronosServer/Controllers/ClientMessageController.cs new file mode 100644 index 0000000..c68a3be --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Controllers/ClientMessageController.cs @@ -0,0 +1,42 @@ +using MareSynchronos.API.SignalR; +using MareSynchronosServer.Hubs; +using MareSynchronosShared.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; + +namespace MareSynchronosServer.Controllers; + +[Route("/msgc")] +[Authorize(Policy = "Internal")] +public class ClientMessageController : Controller +{ + private ILogger _logger; + private IHubContext _hubContext; + + public ClientMessageController(ILogger logger, IHubContext hubContext) + { + _logger = logger; + _hubContext = hubContext; + } + + [Route("sendMessage")] + [HttpPost] + public async Task SendMessage([FromBody] ClientMessage msg) + { + bool hasUid = !string.IsNullOrEmpty(msg.UID); + + if (!hasUid) + { + _logger.LogInformation("Sending Message of severity {severity} to all online users: {message}", msg.Severity, msg.Message); + await _hubContext.Clients.All.Client_ReceiveServerMessage(msg.Severity, msg.Message).ConfigureAwait(false); + } + else + { + _logger.LogInformation("Sending Message of severity {severity} to user {uid}: {message}", msg.Severity, msg.UID, msg.Message); + await _hubContext.Clients.User(msg.UID).Client_ReceiveServerMessage(msg.Severity, msg.Message).ConfigureAwait(false); + } + + return Empty; + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Controllers/MainController.cs b/MareSynchronosServer/MareSynchronosServer/Controllers/MainController.cs new file mode 100644 index 0000000..add64a4 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Controllers/MainController.cs @@ -0,0 +1,31 @@ +using MareSynchronos.API.Routes; +using MareSynchronos.API.SignalR; +using MareSynchronosServer.Hubs; +using MareSynchronosServer.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; + +namespace MareSynchronosServer.Controllers; + +[Route(MareFiles.Main)] +public class MainController : Controller +{ + private IHubContext _hubContext; + + public MainController(ILogger logger, IHubContext hubContext) + { + _hubContext = hubContext; + } + + [HttpGet(MareFiles.Main_SendReady)] + [Authorize(Policy = "Internal")] + public IActionResult SendReadyToClients(string uid, Guid requestId) + { + _ = Task.Run(async () => + { + await _hubContext.Clients.User(uid).Client_DownloadReady(requestId); + }); + return Ok(); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.CharaData.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.CharaData.cs new file mode 100644 index 0000000..4c67ac6 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.CharaData.cs @@ -0,0 +1,638 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Dto.CharaData; +using MareSynchronosServer.Utils; +using MareSynchronosShared.Models; +using MareSynchronosShared.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.EntityFrameworkCore; +using System.Text.Json; + +namespace MareSynchronosServer.Hubs; + +public partial class MareHub +{ + [Authorize(Policy = "Identified")] + public async Task CharaDataCreate() + { + _logger.LogCallInfo(); + + int uploadCount = DbContext.CharaData.Count(c => c.UploaderUID == UserUID); + User user = DbContext.Users.Single(u => u.UID == UserUID); + int maximumUploads = _maxCharaDataByUser; + if (uploadCount >= maximumUploads) + { + return null; + } + + string charaDataId = null; + while (charaDataId == null) + { + charaDataId = StringUtils.GenerateRandomString(10, "abcdefghijklmnopqrstuvwxyzABCDEFHIJKLMNOPQRSTUVWXYZ"); + bool idExists = await DbContext.CharaData.AnyAsync(c => c.UploaderUID == UserUID && c.Id == charaDataId).ConfigureAwait(false); + if (idExists) + { + charaDataId = null; + } + } + + DateTime createdDate = DateTime.UtcNow; + CharaData charaData = new() + { + Id = charaDataId, + UploaderUID = UserUID, + CreatedDate = createdDate, + UpdatedDate = createdDate, + AccessType = CharaDataAccess.Individuals, + ShareType = CharaDataShare.Private, + CustomizeData = string.Empty, + GlamourerData = string.Empty, + ExpiryDate = DateTime.MaxValue, + Description = string.Empty, + }; + + await DbContext.CharaData.AddAsync(charaData).ConfigureAwait(false); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + _logger.LogCallInfo(MareHubLogger.Args("SUCCESS", charaDataId)); + + return GetCharaDataFullDto(charaData); + } + + [Authorize(Policy = "Identified")] + public async Task CharaDataDelete(string id) + { + var existingData = await DbContext.CharaData.SingleOrDefaultAsync(u => u.Id == id && u.UploaderUID == UserUID).ConfigureAwait(false); + if (existingData == null) + return false; + + try + { + _logger.LogCallInfo(MareHubLogger.Args("SUCCESS", id)); + + DbContext.Remove(existingData); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + return true; + } + catch (Exception ex) + { + _logger.LogCallWarning(MareHubLogger.Args("FAILURE", id, ex.Message)); + return false; + } + } + + [Authorize(Policy = "Identified")] + public async Task CharaDataDownload(string id) + { + CharaData charaData = await GetCharaDataById(id, nameof(CharaDataDownload)).ConfigureAwait(false); + + if (!string.Equals(charaData.UploaderUID, UserUID, StringComparison.Ordinal)) + { + charaData.DownloadCount++; + await DbContext.SaveChangesAsync().ConfigureAwait(false); + } + + _logger.LogCallInfo(MareHubLogger.Args("SUCCESS", id)); + + return GetCharaDataDownloadDto(charaData); + } + + [Authorize(Policy = "Identified")] + public async Task CharaDataGetMetainfo(string id) + { + var charaData = await GetCharaDataById(id, nameof(CharaDataGetMetainfo)).ConfigureAwait(false); + + _logger.LogCallInfo(MareHubLogger.Args("SUCCESS", id)); + + return GetCharaDataMetaInfoDto(charaData); + } + + [Authorize(Policy = "Identified")] + public async Task> CharaDataGetOwn() + { + var ownCharaData = await DbContext.CharaData + .Include(u => u.Files) + .Include(u => u.FileSwaps) + .Include(u => u.OriginalFiles) + .Include(u => u.AllowedIndividiuals) + .ThenInclude(u => u.AllowedUser) + .Include(u => u.AllowedIndividiuals) + .ThenInclude(u => u.AllowedGroup) + .Include(u => u.Poses) + .AsSplitQuery() + .Where(c => c.UploaderUID == UserUID).ToListAsync().ConfigureAwait(false); + _logger.LogCallInfo(MareHubLogger.Args("SUCCESS")); + return [.. ownCharaData.Select(GetCharaDataFullDto)]; + } + + [Authorize(Policy = "Identified")] + public async Task CharaDataAttemptRestore(string id) + { + _logger.LogCallInfo(MareHubLogger.Args(id)); + var charaData = await DbContext.CharaData + .Include(u => u.Files) + .Include(u => u.FileSwaps) + .Include(u => u.OriginalFiles) + .Include(u => u.AllowedIndividiuals) + .ThenInclude(u => u.AllowedUser) + .Include(u => u.AllowedIndividiuals) + .ThenInclude(u => u.AllowedGroup) + .Include(u => u.Poses) + .AsSplitQuery() + .SingleOrDefaultAsync(s => s.Id == id && s.UploaderUID == UserUID) + .ConfigureAwait(false); + if (charaData == null) + return null; + + var currentHashes = charaData.Files.Select(f => f.FileCacheHash).ToList(); + var missingFiles = charaData.OriginalFiles.Where(c => !currentHashes.Contains(c.Hash, StringComparer.Ordinal)).ToList(); + + // now let's see what's on the db still + var existingDbFiles = await DbContext.Files + .Where(f => missingFiles.Select(k => k.Hash).Distinct().Contains(f.Hash)) + .ToListAsync() + .ConfigureAwait(false); + + // now shove it all back into the db + foreach (var dbFile in existingDbFiles) + { + var missingFileEntry = missingFiles.First(f => string.Equals(f.Hash, dbFile.Hash, StringComparison.Ordinal)); + charaData.Files.Add(new CharaDataFile() + { + FileCache = dbFile, + GamePath = missingFileEntry.GamePath, + Parent = charaData + }); + missingFiles.Remove(missingFileEntry); + } + + if (existingDbFiles.Any()) + { + await DbContext.SaveChangesAsync().ConfigureAwait(false); + } + + return GetCharaDataFullDto(charaData); + } + + [Authorize(Policy = "Identified")] + public async Task> CharaDataGetShared() + { + _logger.LogCallInfo(); + + List sharedCharaData = []; + var groups = await DbContext.GroupPairs + .Where(u => u.GroupUserUID == UserUID) + .Select(k => k.GroupGID) + .AsNoTracking() + .ToListAsync() + .ConfigureAwait(false); + + var individualPairs = await GetDirectPairedUnpausedUsers().ConfigureAwait(false); + var allPairs = await GetAllPairedUnpausedUsers().ConfigureAwait(false); + + var allSharedDataByPair = await DbContext.CharaData + .Include(u => u.Files) + .Include(u => u.OriginalFiles) + .Include(u => u.AllowedIndividiuals) + .Include(u => u.Poses) + .Include(u => u.Uploader) + .Where(p => p.UploaderUID != UserUID && p.ShareType == CharaDataShare.Shared) + .Where(p => + (individualPairs.Contains(p.UploaderUID) && p.AccessType == CharaDataAccess.ClosePairs) + || (allPairs.Contains(p.UploaderUID) && p.AccessType == CharaDataAccess.AllPairs) + || (p.AllowedIndividiuals.Any(u => u.AllowedUserUID == UserUID || (u.AllowedGroupGID != null && groups.Contains(u.AllowedGroupGID))))) + .AsSplitQuery() + .AsNoTracking() + .ToListAsync() + .ConfigureAwait(false); + + + foreach (var charaData in allSharedDataByPair) + { + sharedCharaData.Add(charaData); + } + + _logger.LogCallInfo(MareHubLogger.Args("SUCCESS", sharedCharaData.Count)); + + return [.. sharedCharaData.Select(GetCharaDataMetaInfoDto)]; + } + + [Authorize(Policy = "Identified")] + public async Task CharaDataUpdate(CharaDataUpdateDto updateDto) + { + var charaData = await DbContext.CharaData + .Include(u => u.Files) + .Include(u => u.OriginalFiles) + .Include(u => u.AllowedIndividiuals) + .ThenInclude(u => u.AllowedUser) + .Include(u => u.AllowedIndividiuals) + .ThenInclude(u => u.AllowedGroup) + .Include(u => u.FileSwaps) + .Include(u => u.Poses) + .AsSplitQuery() + .SingleOrDefaultAsync(u => u.Id == updateDto.Id && u.UploaderUID == UserUID).ConfigureAwait(false); + + if (charaData == null) + return null; + + bool anyChanges = false; + + if (updateDto.Description != null) + { + charaData.Description = updateDto.Description; + anyChanges = true; + } + + if (updateDto.ExpiryDate != null) + { + charaData.ExpiryDate = updateDto.ExpiryDate; + anyChanges = true; + } + + if (updateDto.GlamourerData != null) + { + charaData.GlamourerData = updateDto.GlamourerData; + anyChanges = true; + } + + if (updateDto.CustomizeData != null) + { + charaData.CustomizeData = updateDto.CustomizeData; + anyChanges = true; + } + + if (updateDto.ManipulationData != null) + { + charaData.ManipulationData = updateDto.ManipulationData; + anyChanges = true; + } + + if (updateDto.AccessType != null) + { + charaData.AccessType = GetAccessType(updateDto.AccessType.Value); + anyChanges = true; + } + + if (updateDto.ShareType != null) + { + charaData.ShareType = GetShareType(updateDto.ShareType.Value); + anyChanges = true; + } + + if (updateDto.AllowedUsers != null) + { + var individuals = charaData.AllowedIndividiuals.Where(k => k.AllowedGroup == null).ToList(); + var allowedUserList = updateDto.AllowedUsers.ToList(); + foreach (var user in updateDto.AllowedUsers) + { + if (charaData.AllowedIndividiuals.Any(k => k.AllowedUser != null && (string.Equals(k.AllowedUser.UID, user, StringComparison.Ordinal) || string.Equals(k.AllowedUser.Alias, user, StringComparison.Ordinal)))) + { + continue; + } + else + { + var dbUser = await DbContext.Users.SingleOrDefaultAsync(u => u.UID == user || u.Alias == user).ConfigureAwait(false); + if (dbUser != null) + { + charaData.AllowedIndividiuals.Add(new CharaDataAllowance() + { + AllowedUser = dbUser, + Parent = charaData + }); + } + } + } + + foreach (var dataUser in individuals.Where(k => !updateDto.AllowedUsers.Contains(k.AllowedUser.UID, StringComparer.Ordinal) && !updateDto.AllowedUsers.Contains(k.AllowedUser.Alias, StringComparer.Ordinal))) + { + DbContext.Remove(dataUser); + charaData.AllowedIndividiuals.Remove(dataUser); + } + + anyChanges = true; + } + + if (updateDto.AllowedGroups != null) + { + var individualGroups = charaData.AllowedIndividiuals.Where(k => k.AllowedUser == null).ToList(); + var allowedGroups = updateDto.AllowedGroups.ToList(); + foreach (var group in updateDto.AllowedGroups) + { + if (charaData.AllowedIndividiuals.Any(k => k.AllowedGroup != null && (string.Equals(k.AllowedGroup.GID, group, StringComparison.Ordinal) || string.Equals(k.AllowedGroup.Alias, group, StringComparison.Ordinal)))) + { + continue; + } + else + { + var groupUser = await DbContext.GroupPairs.Include(u => u.Group).SingleOrDefaultAsync(u => (u.Group.GID == group || u.Group.Alias == group) && u.GroupUserUID == UserUID).ConfigureAwait(false); + if (groupUser != null) + { + charaData.AllowedIndividiuals.Add(new CharaDataAllowance() + { + AllowedGroup = groupUser.Group, + Parent = charaData + }); + } + } + } + + foreach (var dataGroup in individualGroups.Where(k => !updateDto.AllowedGroups.Contains(k.AllowedGroup.GID, StringComparer.Ordinal) && !updateDto.AllowedGroups.Contains(k.AllowedGroup.Alias, StringComparer.Ordinal))) + { + DbContext.Remove(dataGroup); + charaData.AllowedIndividiuals.Remove(dataGroup); + } + + anyChanges = true; + } + + if (updateDto.FileGamePaths != null) + { + var originalFiles = charaData.OriginalFiles.ToList(); + charaData.OriginalFiles.Clear(); + DbContext.RemoveRange(originalFiles); + var files = charaData.Files.ToList(); + charaData.Files.Clear(); + DbContext.RemoveRange(files); + foreach (var file in updateDto.FileGamePaths) + { + charaData.Files.Add(new CharaDataFile() + { + FileCacheHash = file.HashOrFileSwap, + GamePath = file.GamePath, + Parent = charaData + }); + + charaData.OriginalFiles.Add(new CharaDataOriginalFile() + { + Hash = file.HashOrFileSwap, + Parent = charaData, + GamePath = file.GamePath + }); + } + + anyChanges = true; + } + + if (updateDto.FileSwaps != null) + { + var fileSwaps = charaData.FileSwaps.ToList(); + charaData.FileSwaps.Clear(); + DbContext.RemoveRange(fileSwaps); + foreach (var file in updateDto.FileSwaps) + { + charaData.FileSwaps.Add(new CharaDataFileSwap() + { + FilePath = file.HashOrFileSwap, + GamePath = file.GamePath, + Parent = charaData + }); + } + + anyChanges = true; + } + + if (updateDto.Poses != null) + { + foreach (var pose in updateDto.Poses) + { + if (pose.Id == null) + { + charaData.Poses.Add(new CharaDataPose() + { + Description = pose.Description, + Parent = charaData, + ParentUploaderUID = UserUID, + PoseData = pose.PoseData, + WorldData = pose.WorldData == null ? string.Empty : JsonSerializer.Serialize(pose.WorldData), + }); + + anyChanges = true; + } + else + { + var associatedPose = charaData.Poses.FirstOrDefault(p => p.Id == pose.Id); + if (associatedPose == null) + continue; + + if (pose.Description == null && pose.PoseData == null && pose.WorldData == null) + { + charaData.Poses.Remove(associatedPose); + DbContext.Remove(associatedPose); + } + else + { + if (pose.Description != null) + associatedPose.Description = pose.Description; + if (pose.WorldData != null) + { + if (pose.WorldData.Value == default) associatedPose.WorldData = string.Empty; + else associatedPose.WorldData = JsonSerializer.Serialize(pose.WorldData.Value); + } + if (pose.PoseData != null) + associatedPose.PoseData = pose.PoseData; + } + + anyChanges = true; + } + + var overflowingPoses = charaData.Poses.Skip(10).ToList(); + foreach (var overflowing in overflowingPoses) + { + charaData.Poses.Remove(overflowing); + DbContext.Remove(overflowing); + } + } + } + + if (anyChanges) + { + charaData.UpdatedDate = DateTime.UtcNow; + await DbContext.SaveChangesAsync().ConfigureAwait(false); + _logger.LogCallInfo(MareHubLogger.Args("SUCCESS", anyChanges)); + } + + return GetCharaDataFullDto(charaData); + } + + private static CharaDataAccess GetAccessType(AccessTypeDto dataAccess) => dataAccess switch + { + AccessTypeDto.Public => CharaDataAccess.Public, + AccessTypeDto.AllPairs => CharaDataAccess.AllPairs, + AccessTypeDto.ClosePairs => CharaDataAccess.ClosePairs, + AccessTypeDto.Individuals => CharaDataAccess.Individuals, + _ => throw new NotSupportedException(), + }; + + private static AccessTypeDto GetAccessTypeDto(CharaDataAccess dataAccess) => dataAccess switch + { + CharaDataAccess.Public => AccessTypeDto.Public, + CharaDataAccess.AllPairs => AccessTypeDto.AllPairs, + CharaDataAccess.ClosePairs => AccessTypeDto.ClosePairs, + CharaDataAccess.Individuals => AccessTypeDto.Individuals, + _ => throw new NotSupportedException(), + }; + + private static CharaDataDownloadDto GetCharaDataDownloadDto(CharaData charaData) + { + return new CharaDataDownloadDto(charaData.Id, charaData.Uploader.ToUserData()) + { + CustomizeData = charaData.CustomizeData, + Description = charaData.Description, + FileGamePaths = charaData.Files.Select(k => new GamePathEntry(k.FileCacheHash, k.GamePath)).ToList(), + GlamourerData = charaData.GlamourerData, + FileSwaps = charaData.FileSwaps.Select(k => new GamePathEntry(k.FilePath, k.GamePath)).ToList(), + ManipulationData = charaData.ManipulationData, + }; + } + + private CharaDataFullDto GetCharaDataFullDto(CharaData charaData) + { + return new CharaDataFullDto(charaData.Id, new(UserUID)) + { + AccessType = GetAccessTypeDto(charaData.AccessType), + ShareType = GetShareTypeDto(charaData.ShareType), + AllowedUsers = [.. charaData.AllowedIndividiuals.Where(k => !string.IsNullOrEmpty(k.AllowedUserUID)).Select(u => new UserData(u.AllowedUser.UID, u.AllowedUser.Alias))], + AllowedGroups = [.. charaData.AllowedIndividiuals.Where(k => !string.IsNullOrEmpty(k.AllowedGroupGID)).Select(k => new GroupData(k.AllowedGroup.GID, k.AllowedGroup.Alias))], + CustomizeData = charaData.CustomizeData, + Description = charaData.Description, + ExpiryDate = charaData.ExpiryDate ?? DateTime.MaxValue, + OriginalFiles = charaData.OriginalFiles.Select(k => new GamePathEntry(k.Hash, k.GamePath)).ToList(), + FileGamePaths = charaData.Files.Select(k => new GamePathEntry(k.FileCacheHash, k.GamePath)).ToList(), + FileSwaps = charaData.FileSwaps.Select(k => new GamePathEntry(k.FilePath, k.GamePath)).ToList(), + GlamourerData = charaData.GlamourerData, + CreatedDate = charaData.CreatedDate, + UpdatedDate = charaData.UpdatedDate, + ManipulationData = charaData.ManipulationData, + DownloadCount = charaData.DownloadCount, + PoseData = [.. charaData.Poses.OrderBy(p => p.Id).Select(k => + { + WorldData data = default; + + if(!string.IsNullOrEmpty(k.WorldData)) data = JsonSerializer.Deserialize(k.WorldData); + return new PoseEntry(k.Id) + { + Description = k.Description, + PoseData = k.PoseData, + WorldData = data + }; + })], + }; + } + + private static CharaDataMetaInfoDto GetCharaDataMetaInfoDto(CharaData charaData) + { + var allOrigHashes = charaData.OriginalFiles.Select(k => k.Hash).ToList(); + var allFileHashes = charaData.Files.Select(f => f.FileCacheHash).ToList(); + var allHashesPresent = allOrigHashes.TrueForAll(h => allFileHashes.Contains(h, StringComparer.Ordinal)); + var canBeDownloaded = allHashesPresent &= !string.IsNullOrEmpty(charaData.GlamourerData); + return new CharaDataMetaInfoDto(charaData.Id, charaData.Uploader.ToUserData()) + { + CanBeDownloaded = canBeDownloaded, + Description = charaData.Description, + UpdatedDate = charaData.UpdatedDate, + PoseData = [.. charaData.Poses.OrderBy(p => p.Id).Select(k => + { + WorldData data = default; + if(!string.IsNullOrEmpty(k.WorldData)) data = JsonSerializer.Deserialize(k.WorldData); + return new PoseEntry(k.Id) + { + Description = k.Description, + PoseData = k.PoseData, + WorldData = data + }; + })], + }; + } + + private static CharaDataShare GetShareType(ShareTypeDto dataShare) => dataShare switch + { + ShareTypeDto.Shared => CharaDataShare.Shared, + ShareTypeDto.Private => CharaDataShare.Private, + _ => throw new NotSupportedException(), + }; + + private static ShareTypeDto GetShareTypeDto(CharaDataShare dataShare) => dataShare switch + { + CharaDataShare.Shared => ShareTypeDto.Shared, + CharaDataShare.Private => ShareTypeDto.Private, + _ => throw new NotSupportedException(), + }; + + private async Task CheckCharaDataAllowance(CharaData charaData, List joinedGroups) + { + // check for self + if (string.Equals(charaData.UploaderUID, UserUID, StringComparison.Ordinal)) + return true; + + // check for public access + if (charaData.AccessType == CharaDataAccess.Public) + return true; + + // check for individuals + if (charaData.AllowedIndividiuals.Any(u => string.Equals(u.AllowedUserUID, UserUID, StringComparison.Ordinal))) + return true; + + var pairInfoUploader = await GetAllPairedUnpausedUsers(charaData.UploaderUID).ConfigureAwait(false); + + // check for all pairs + if (charaData.AccessType == CharaDataAccess.AllPairs) + { + if (pairInfoUploader.Any(pair => string.Equals(pair, UserUID, StringComparison.Ordinal))) + return true; + + return false; + } + + // check for individual pairs + if (charaData.AccessType == CharaDataAccess.ClosePairs) + { + if (pairInfoUploader.Any(pair => string.Equals(pair, UserUID, StringComparison.Ordinal))) + { + ClientPair callerPair = + await DbContext.ClientPairs.AsNoTracking().SingleOrDefaultAsync(w => w.UserUID == UserUID && w.OtherUserUID == charaData.UploaderUID).ConfigureAwait(false); + ClientPair uploaderPair = + await DbContext.ClientPairs.AsNoTracking().SingleOrDefaultAsync(w => w.UserUID == charaData.UploaderUID && w.OtherUserUID == UserUID).ConfigureAwait(false); + return (callerPair != null && uploaderPair != null); + } + + return false; + } + + return false; + } + + private async Task GetCharaDataById(string id, string methodName) + { + var splitid = id.Split(":", StringSplitOptions.None); + if (splitid.Length != 2) + { + _logger.LogCallWarning(MareHubLogger.Args("INVALID", id)); + throw new InvalidOperationException($"Id {id} not in expected format"); + } + + var charaData = await DbContext.CharaData + .Include(u => u.Files) + .Include(u => u.FileSwaps) + .Include(u => u.AllowedIndividiuals) + .Include(u => u.Poses) + .Include(u => u.Uploader) + .AsSplitQuery() + .SingleOrDefaultAsync(c => c.Id == splitid[1] && c.UploaderUID == splitid[0]).ConfigureAwait(false); + + if (charaData == null) + { + _logger.LogCallWarning(MareHubLogger.Args("NOT FOUND", id)); + throw new InvalidDataException($"No chara data with {id} found"); + } + + var groups = await DbContext.GroupPairs.Where(u => u.GroupUserUID == UserUID).Select(k => k.GroupGID).ToListAsync() + .ConfigureAwait(false); + + if (!await CheckCharaDataAllowance(charaData, groups).ConfigureAwait(false)) + { + _logger.LogCallWarning(MareHubLogger.Args("UNAUTHORIZED", id)); + throw new UnauthorizedAccessException($"User is not allowed to download {id}"); + } + + return charaData; + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Chat.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Chat.cs new file mode 100644 index 0000000..41e2aa1 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Chat.cs @@ -0,0 +1,52 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.API.Dto.User; +using MareSynchronosServer.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; + +namespace MareSynchronosServer.Hubs; + +public partial class MareHub +{ + [Authorize(Policy = "Identified")] + public Task UserChatSendMsg(UserDto dto, ChatMessage message) + { + _logger.LogCallInfo(MareHubLogger.Args(dto)); + // TODO + return Task.CompletedTask; + } + + [Authorize(Policy = "Identified")] + public async Task GroupChatSendMsg(GroupDto dto, ChatMessage message) + { + _logger.LogCallInfo(MareHubLogger.Args(dto)); + + var (userExists, groupPair) = await TryValidateUserInGroup(dto.GID, UserUID).ConfigureAwait(false); + if (!userExists) return; + + var group = await DbContext.Groups.AsNoTracking().SingleAsync(g => g.GID == dto.GID).ConfigureAwait(false); + var sender = await DbContext.Users.AsNoTracking().SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); + var groupPairs = await DbContext.GroupPairs.AsNoTracking().Include(p => p.GroupUser).Where(p => p.GroupGID == dto.Group.GID).ToListAsync().ConfigureAwait(false); + + if (group == null || sender == null) return; + + // TODO: Add and check chat permissions + if (group.Alias?.Equals("Snowcloak", StringComparison.Ordinal) ?? false) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, $"Chat is disabled for syncshell '{dto.GroupAliasOrGID}'.").ConfigureAwait(false); + return; + } + + // TOOO: Sign the message + var signedMessage = new SignedChatMessage(message, sender.ToUserData()) + { + Timestamp = 0, + Signature = "", + }; + + await Clients.Users(groupPairs.Select(p => p.GroupUserUID)).Client_GroupChatMsg(new(new(group.ToGroupData()), signedMessage)).ConfigureAwait(false); + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.ClientStubs.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.ClientStubs.cs new file mode 100644 index 0000000..29d64e5 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.ClientStubs.cs @@ -0,0 +1,63 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Dto; +using MareSynchronos.API.Dto.CharaData; +using MareSynchronos.API.Dto.Chat; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.API.Dto.User; + +namespace MareSynchronosServer.Hubs +{ + public partial class MareHub + { + public Task Client_DownloadReady(Guid requestId) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_GroupChangePermissions(GroupPermissionDto groupPermission) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_GroupChatMsg(GroupChatMsgDto groupChatMsgDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_GroupDelete(GroupDto groupDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_GroupPairChangePermissions(GroupPairUserPermissionDto permissionDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_GroupPairChangeUserInfo(GroupPairUserInfoDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_GroupPairJoined(GroupPairFullInfoDto groupPairInfoDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_GroupPairLeft(GroupPairDto groupPairDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_GroupSendFullInfo(GroupFullInfoDto groupInfo) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_GroupSendInfo(GroupInfoDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_ReceiveServerMessage(MessageSeverity messageSeverity, string message) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_UserAddClientPair(UserPairDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_UserChatMsg(UserChatMsgDto userChatMsgDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dataDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_UserReceiveUploadStatus(UserDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_UserRemoveClientPair(UserDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_UserSendOffline(UserDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_UserSendOnline(OnlineUserIdentDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_UserUpdateOtherPairPermissions(UserPermissionsDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_UserUpdateProfile(UserDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_UserUpdateSelfPairPermissions(UserPermissionsDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_GposeLobbyJoin(UserData userData) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + public Task Client_GposeLobbyLeave(UserData userData) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + public Task Client_GposeLobbyPushCharacterData(CharaDataDownloadDto charaDownloadDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + public Task Client_GposeLobbyPushPoseData(UserData userData, PoseData poseData) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + public Task Client_GposeLobbyPushWorldData(UserData userData, WorldData worldData) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Functions.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Functions.cs new file mode 100644 index 0000000..c1a7944 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Functions.cs @@ -0,0 +1,295 @@ +using MareSynchronosShared.Models; +using Microsoft.EntityFrameworkCore; +using MareSynchronosServer.Utils; +using MareSynchronosShared.Utils; +using MareSynchronos.API.Data; +using MareSynchronos.API.Dto.Group; +using MareSynchronosShared.Metrics; +using Microsoft.AspNetCore.SignalR; + +namespace MareSynchronosServer.Hubs; + +public partial class MareHub +{ + public string UserCharaIdent => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, MareClaimTypes.CharaIdent, StringComparison.Ordinal))?.Value ?? throw new Exception("No Chara Ident in Claims"); + + public string UserUID => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, MareClaimTypes.Uid, StringComparison.Ordinal))?.Value ?? throw new Exception("No UID in Claims"); + + public string Continent => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, MareClaimTypes.Continent, StringComparison.Ordinal))?.Value ?? "UNK"; + + private async Task DeleteUser(User user) + { + var ownPairData = await DbContext.ClientPairs.Where(u => u.User.UID == user.UID).ToListAsync().ConfigureAwait(false); + var auth = await DbContext.Auth.SingleAsync(u => u.UserUID == user.UID).ConfigureAwait(false); + var lodestone = await DbContext.LodeStoneAuth.SingleOrDefaultAsync(a => a.User.UID == user.UID).ConfigureAwait(false); + var groupPairs = await DbContext.GroupPairs.Where(g => g.GroupUserUID == user.UID).ToListAsync().ConfigureAwait(false); + var userProfileData = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == user.UID).ConfigureAwait(false); + var bannedEntries = await DbContext.GroupBans.Where(u => u.BannedUserUID == user.UID).ToListAsync().ConfigureAwait(false); + + if (lodestone != null) + { + DbContext.Remove(lodestone); + } + + if (userProfileData != null) + { + DbContext.Remove(userProfileData); + } + + DbContext.ClientPairs.RemoveRange(ownPairData); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + var otherPairData = await DbContext.ClientPairs.Include(u => u.User) + .Where(u => u.OtherUser.UID == user.UID).AsNoTracking().ToListAsync().ConfigureAwait(false); + foreach (var pair in otherPairData) + { + await Clients.User(pair.UserUID).Client_UserRemoveClientPair(new(user.ToUserData())).ConfigureAwait(false); + } + + foreach (var pair in groupPairs) + { + await UserLeaveGroup(new GroupDto(new GroupData(pair.GroupGID)), user.UID).ConfigureAwait(false); + } + + _mareMetrics.IncCounter(MetricsAPI.CounterUsersRegisteredDeleted, 1); + + DbContext.GroupBans.RemoveRange(bannedEntries); + DbContext.ClientPairs.RemoveRange(otherPairData); + DbContext.Users.Remove(user); + DbContext.Auth.Remove(auth); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + } + + private async Task> GetAllPairedClientsWithPauseState(string? uid = null) + { + uid ??= UserUID; + + var query = await (from userPair in DbContext.ClientPairs + join otherUserPair in DbContext.ClientPairs on userPair.OtherUserUID equals otherUserPair.UserUID + where otherUserPair.OtherUserUID == uid && userPair.UserUID == uid + select new + { + UID = Convert.ToString(userPair.OtherUserUID), + GID = "DIRECT", + PauseStateSelf = userPair.IsPaused, + PauseStateOther = otherUserPair.IsPaused, + }) + .Union( + (from userGroupPair in DbContext.GroupPairs + join otherGroupPair in DbContext.GroupPairs on userGroupPair.GroupGID equals otherGroupPair.GroupGID + where + userGroupPair.GroupUserUID == uid + && otherGroupPair.GroupUserUID != uid + select new + { + UID = Convert.ToString(otherGroupPair.GroupUserUID), + GID = Convert.ToString(otherGroupPair.GroupGID), + PauseStateSelf = userGroupPair.IsPaused, + PauseStateOther = otherGroupPair.IsPaused, + }) + ).AsNoTracking().ToListAsync().ConfigureAwait(false); + + return query.GroupBy(g => g.UID, g => (g.GID, g.PauseStateSelf, g.PauseStateOther), + (key, g) => new PausedEntry + { + UID = key, + PauseStates = g.Select(p => new PauseState() { GID = string.Equals(p.GID, "DIRECT", StringComparison.Ordinal) ? null : p.GID, IsSelfPaused = p.PauseStateSelf, IsOtherPaused = p.PauseStateOther }) + .ToList(), + }, StringComparer.Ordinal).ToList(); + } + + private async Task> GetAllPairedUnpausedUsers(string? uid = null) + { + uid ??= UserUID; + var ret = await GetAllPairedClientsWithPauseState(uid).ConfigureAwait(false); + return ret.Where(k => !k.IsPaused).Select(k => k.UID).ToList(); + } + + private async Task> GetDirectPairedUnpausedUsers(string? uid = null) + { + uid ??= UserUID; + + var query = await (from userPair in DbContext.ClientPairs + join otherUserPair in DbContext.ClientPairs on userPair.OtherUserUID equals otherUserPair.UserUID + where otherUserPair.OtherUserUID == uid && userPair.UserUID == uid && !userPair.IsPaused && !otherUserPair.IsPaused + select Convert.ToString(userPair.OtherUserUID)).AsNoTracking().ToListAsync().ConfigureAwait(false); + + return query; + } + + private async Task> GetOnlineUsers(List uids) + { + var result = await _redis.GetAllAsync(uids.Select(u => "UID:" + u).ToHashSet(StringComparer.Ordinal)).ConfigureAwait(false); + return uids.Where(u => result.TryGetValue("UID:" + u, out var ident) && !string.IsNullOrEmpty(ident)).ToDictionary(u => u, u => result["UID:" + u], StringComparer.Ordinal); + } + + private async Task GetUserIdent(string uid) + { + if (string.IsNullOrEmpty(uid)) return string.Empty; + return await _redis.GetAsync("UID:" + uid).ConfigureAwait(false); + } + + private async Task RemoveUserFromRedis() + { + await _redis.RemoveAsync("UID:" + UserUID, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false); + } + + private async Task SendGroupDeletedToAll(List groupUsers) + { + foreach (var pair in groupUsers) + { + var pairIdent = await GetUserIdent(pair.GroupUserUID).ConfigureAwait(false); + if (string.IsNullOrEmpty(pairIdent)) continue; + + var pairs = await GetAllPairedClientsWithPauseState(pair.GroupUserUID).ConfigureAwait(false); + + foreach (var groupUserPair in groupUsers.Where(g => !string.Equals(g.GroupUserUID, pair.GroupUserUID, StringComparison.Ordinal))) + { + await UserGroupLeave(groupUserPair, pairs, pairIdent, pair.GroupUserUID).ConfigureAwait(false); + } + } + } + + private async Task> SendOfflineToAllPairedUsers() + { + var usersToSendDataTo = await GetAllPairedUnpausedUsers().ConfigureAwait(false); + var self = await DbContext.Users.AsNoTracking().SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); + await Clients.Users(usersToSendDataTo).Client_UserSendOffline(new(self.ToUserData())).ConfigureAwait(false); + + return usersToSendDataTo; + } + + private async Task> SendOnlineToAllPairedUsers() + { + var usersToSendDataTo = await GetAllPairedUnpausedUsers().ConfigureAwait(false); + var self = await DbContext.Users.AsNoTracking().SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); + await Clients.Users(usersToSendDataTo).Client_UserSendOnline(new(self.ToUserData(), UserCharaIdent)).ConfigureAwait(false); + + return usersToSendDataTo; + } + + private async Task<(bool IsValid, Group ReferredGroup)> TryValidateGroupModeratorOrOwner(string gid) + { + var isOwnerResult = await TryValidateOwner(gid).ConfigureAwait(false); + if (isOwnerResult.isValid) return (true, isOwnerResult.ReferredGroup); + + if (isOwnerResult.ReferredGroup == null) return (false, null); + + var groupPairSelf = await DbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == UserUID).ConfigureAwait(false); + if (groupPairSelf == null || !groupPairSelf.IsModerator) return (false, null); + + return (true, isOwnerResult.ReferredGroup); + } + + private async Task<(bool isValid, Group ReferredGroup)> TryValidateOwner(string gid) + { + var group = await DbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false); + if (group == null) return (false, null); + + return (string.Equals(group.OwnerUID, UserUID, StringComparison.Ordinal), group); + } + + private async Task<(bool IsValid, GroupPair ReferredPair)> TryValidateUserInGroup(string gid, string? uid = null) + { + uid ??= UserUID; + + var groupPair = await DbContext.GroupPairs.Include(c => c.GroupUser) + .SingleOrDefaultAsync(g => g.GroupGID == gid && (g.GroupUserUID == uid || g.GroupUser.Alias == uid)).ConfigureAwait(false); + if (groupPair == null) return (false, null); + + return (true, groupPair); + } + + private async Task UpdateUserOnRedis() + { + await _redis.AddAsync("UID:" + UserUID, UserCharaIdent, TimeSpan.FromSeconds(60), StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false); + } + + private async Task UserGroupLeave(GroupPair groupUserPair, List allUserPairs, string userIdent, string? uid = null) + { + uid ??= UserUID; + var userPair = allUserPairs.SingleOrDefault(p => string.Equals(p.UID, groupUserPair.GroupUserUID, StringComparison.Ordinal)); + if (userPair != null) + { + if (userPair.IsDirectlyPaused != PauseInfo.NoConnection) return; + if (userPair.IsPausedPerGroup is PauseInfo.Unpaused) return; + } + + var groupUserIdent = await GetUserIdent(groupUserPair.GroupUserUID).ConfigureAwait(false); + if (!string.IsNullOrEmpty(groupUserIdent)) + { + await Clients.User(uid).Client_UserSendOffline(new(new(groupUserPair.GroupUserUID))).ConfigureAwait(false); + await Clients.User(groupUserPair.GroupUserUID).Client_UserSendOffline(new(new(uid))).ConfigureAwait(false); + } + } + + private async Task UserLeaveGroup(GroupDto dto, string userUid) + { + _logger.LogCallInfo(MareHubLogger.Args(dto)); + + var (exists, groupPair) = await TryValidateUserInGroup(dto.Group.GID, userUid).ConfigureAwait(false); + if (!exists) return; + + var group = await DbContext.Groups.SingleOrDefaultAsync(g => g.GID == dto.Group.GID).ConfigureAwait(false); + + var groupPairs = await DbContext.GroupPairs.Where(p => p.GroupGID == group.GID).ToListAsync().ConfigureAwait(false); + var groupPairsWithoutSelf = groupPairs.Where(p => !string.Equals(p.GroupUserUID, userUid, StringComparison.Ordinal)).ToList(); + + DbContext.GroupPairs.Remove(groupPair); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + await Clients.User(userUid).Client_GroupDelete(new GroupDto(group.ToGroupData())).ConfigureAwait(false); + + bool ownerHasLeft = string.Equals(group.OwnerUID, userUid, StringComparison.Ordinal); + if (ownerHasLeft) + { + if (!groupPairsWithoutSelf.Any()) + { + _logger.LogCallInfo(MareHubLogger.Args(dto, "Deleted")); + + DbContext.Groups.Remove(group); + } + else + { + var groupHasMigrated = await SharedDbFunctions.MigrateOrDeleteGroup(DbContext, group, groupPairsWithoutSelf, _maxExistingGroupsByUser).ConfigureAwait(false); + + if (groupHasMigrated.Item1) + { + _logger.LogCallInfo(MareHubLogger.Args(dto, "Migrated", groupHasMigrated.Item2)); + + var user = await DbContext.Users.SingleAsync(u => u.UID == groupHasMigrated.Item2).ConfigureAwait(false); + + await Clients.Users(groupPairsWithoutSelf.Select(p => p.GroupUserUID)).Client_GroupSendInfo(new GroupInfoDto(group.ToGroupData(), + user.ToUserData(), group.GetGroupPermissions())).ConfigureAwait(false); + } + else + { + _logger.LogCallInfo(MareHubLogger.Args(dto, "Deleted")); + + await Clients.Users(groupPairsWithoutSelf.Select(p => p.GroupUserUID)).Client_GroupDelete(dto).ConfigureAwait(false); + + await SendGroupDeletedToAll(groupPairs).ConfigureAwait(false); + + return; + } + } + } + + var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == userUid).ToListAsync().ConfigureAwait(false); + DbContext.CharaDataAllowances.RemoveRange(sharedData); + + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + _logger.LogCallInfo(MareHubLogger.Args(dto, "Success")); + + await Clients.Users(groupPairsWithoutSelf.Select(p => p.GroupUserUID)).Client_GroupPairLeft(new GroupPairDto(dto.Group, groupPair.GroupUser.ToUserData())).ConfigureAwait(false); + + var allUserPairs = await GetAllPairedClientsWithPauseState().ConfigureAwait(false); + + var ident = await GetUserIdent(userUid).ConfigureAwait(false); + + foreach (var groupUserPair in groupPairsWithoutSelf) + { + await UserGroupLeave(groupUserPair, allUserPairs, ident, userUid).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.GposeLobby.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.GposeLobby.cs new file mode 100644 index 0000000..b6f0f1a --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.GposeLobby.cs @@ -0,0 +1,155 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Dto.CharaData; +using MareSynchronosServer.Utils; +using MareSynchronosShared.Metrics; +using MareSynchronosShared.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; + +namespace MareSynchronosServer.Hubs; + +public partial class MareHub +{ + private async Task GetUserGposeLobby() + { + return await _redis.GetAsync(GposeLobbyUser).ConfigureAwait(false); + } + + private async Task> GetUsersInLobby(string lobbyId, bool includeSelf = false) + { + var users = await _redis.GetAsync>($"GposeLobby:{lobbyId}").ConfigureAwait(false); + return users?.Where(u => includeSelf || !string.Equals(u, UserUID, StringComparison.Ordinal)).ToList() ?? []; + } + + private async Task AddUserToLobby(string lobbyId, List priorUsers) + { + _mareMetrics.IncGauge(MetricsAPI.GaugeGposeLobbyUsers); + if (priorUsers.Count == 0) + _mareMetrics.IncGauge(MetricsAPI.GaugeGposeLobbies); + + await _redis.AddAsync(GposeLobbyUser, lobbyId).ConfigureAwait(false); + await _redis.AddAsync($"GposeLobby:{lobbyId}", priorUsers.Concat([UserUID])).ConfigureAwait(false); + } + + private async Task RemoveUserFromLobby(string lobbyId, List priorUsers) + { + await _redis.RemoveAsync(GposeLobbyUser).ConfigureAwait(false); + + _mareMetrics.DecGauge(MetricsAPI.GaugeGposeLobbyUsers); + + if (priorUsers.Count == 1) + { + await _redis.RemoveAsync($"GposeLobby:{lobbyId}").ConfigureAwait(false); + _mareMetrics.DecGauge(MetricsAPI.GaugeGposeLobbies); + } + else + { + priorUsers.Remove(UserUID); + await _redis.AddAsync($"GposeLobby:{lobbyId}", priorUsers).ConfigureAwait(false); + await Clients.Users(priorUsers).Client_GposeLobbyLeave(new(UserUID)).ConfigureAwait(false); + } + } + + private string GposeLobbyUser => $"GposeLobbyUser:{UserUID}"; + + + [Authorize(Policy = "Identified")] + public async Task GposeLobbyCreate() + { + _logger.LogCallInfo(); + var alreadyInLobby = await GetUserGposeLobby().ConfigureAwait(false); + if (!string.IsNullOrEmpty(alreadyInLobby)) + { + throw new HubException("Already in GPose Lobby, cannot join another"); + } + + string lobbyId = string.Empty; + while (string.IsNullOrEmpty(lobbyId)) + { + lobbyId = StringUtils.GenerateRandomString(30, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"); + var result = await _redis.GetAsync>($"GposeLobby:{lobbyId}").ConfigureAwait(false); + if (result != null) + lobbyId = string.Empty; + } + + await AddUserToLobby(lobbyId, []).ConfigureAwait(false); + + return lobbyId; + } + + [Authorize(Policy = "Identified")] + public async Task> GposeLobbyJoin(string lobbyId) + { + _logger.LogCallInfo(); + var existingLobbyId = await GetUserGposeLobby().ConfigureAwait(false); + if (!string.IsNullOrEmpty(existingLobbyId)) + await GposeLobbyLeave().ConfigureAwait(false); + + var lobbyUsers = await GetUsersInLobby(lobbyId).ConfigureAwait(false); + if (!lobbyUsers.Any()) + return []; + + await AddUserToLobby(lobbyId, lobbyUsers).ConfigureAwait(false); + + var user = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); + await Clients.Users(lobbyUsers.Where(u => !string.Equals(u, UserUID, StringComparison.Ordinal))) + .Client_GposeLobbyJoin(user.ToUserData()).ConfigureAwait(false); + + var users = await DbContext.Users.Where(u => lobbyUsers.Contains(u.UID)) + .Select(u => u.ToUserData()) + .ToListAsync() + .ConfigureAwait(false); + + return users; + } + + [Authorize(Policy = "Identified")] + public async Task GposeLobbyLeave() + { + var lobbyId = await GetUserGposeLobby().ConfigureAwait(false); + if (string.IsNullOrEmpty(lobbyId)) + return true; + + _logger.LogCallInfo(); + + var lobbyUsers = await GetUsersInLobby(lobbyId, true).ConfigureAwait(false); + await RemoveUserFromLobby(lobbyId, lobbyUsers).ConfigureAwait(false); + + return true; + } + + [Authorize(Policy = "Identified")] + public async Task GposeLobbyPushCharacterData(CharaDataDownloadDto charaDataDownloadDto) + { + _logger.LogCallInfo(); + var lobbyId = await GetUserGposeLobby().ConfigureAwait(false); + if (string.IsNullOrEmpty(lobbyId)) + return; + + var lobbyUsers = await GetUsersInLobby(lobbyId).ConfigureAwait(false); + await Clients.Users(lobbyUsers).Client_GposeLobbyPushCharacterData(charaDataDownloadDto).ConfigureAwait(false); + } + + [Authorize(Policy = "Identified")] + public async Task GposeLobbyPushPoseData(PoseData poseData) + { + _logger.LogCallInfo(); + var lobbyId = await GetUserGposeLobby().ConfigureAwait(false); + if (string.IsNullOrEmpty(lobbyId)) + return; + + await _gPoseLobbyDistributionService.PushPoseData(lobbyId, UserUID, poseData).ConfigureAwait(false); + } + + [Authorize(Policy = "Identified")] + public async Task GposeLobbyPushWorldData(WorldData worldData) + { + _logger.LogCallInfo(); + var lobbyId = await GetUserGposeLobby().ConfigureAwait(false); + if (string.IsNullOrEmpty(lobbyId)) + return; + + await _gPoseLobbyDistributionService.PushWorldData(lobbyId, UserUID, worldData).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Groups.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Groups.cs new file mode 100644 index 0000000..82b3b79 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Groups.cs @@ -0,0 +1,562 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.API.Dto.Chat; +using MareSynchronos.API.Dto.Group; +using MareSynchronosServer.Utils; +using MareSynchronosShared.Models; +using MareSynchronosShared.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using System.Security.Cryptography; + +namespace MareSynchronosServer.Hubs; + +public partial class MareHub +{ + [Authorize(Policy = "Identified")] + public async Task GroupBanUser(GroupPairDto dto, string reason) + { + _logger.LogCallInfo(MareHubLogger.Args(dto, reason)); + + var (userHasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false); + if (!userHasRights) return; + + var (userExists, groupPair) = await TryValidateUserInGroup(dto.Group.GID, dto.User.UID).ConfigureAwait(false); + if (!userExists) return; + + if (groupPair.IsModerator || string.Equals(group.OwnerUID, dto.User.UID, StringComparison.Ordinal)) return; + + var alias = string.IsNullOrEmpty(groupPair.GroupUser.Alias) ? "-" : groupPair.GroupUser.Alias; + var ban = new GroupBan() + { + BannedByUID = UserUID, + BannedReason = $"{reason} (Alias at time of ban: {alias})", + BannedOn = DateTime.UtcNow, + BannedUserUID = dto.User.UID, + GroupGID = dto.Group.GID, + }; + + DbContext.Add(ban); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + await GroupRemoveUser(dto).ConfigureAwait(false); + + _logger.LogCallInfo(MareHubLogger.Args(dto, "Success")); + } + + [Authorize(Policy = "Identified")] + public async Task GroupChangeGroupPermissionState(GroupPermissionDto dto) + { + _logger.LogCallInfo(MareHubLogger.Args(dto)); + + var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false); + if (!hasRights) return; + + group.InvitesEnabled = !dto.Permissions.HasFlag(GroupPermissions.DisableInvites); + group.DisableSounds = dto.Permissions.HasFlag(GroupPermissions.DisableSounds); + group.DisableAnimations = dto.Permissions.HasFlag(GroupPermissions.DisableAnimations); + group.DisableVFX = dto.Permissions.HasFlag(GroupPermissions.DisableVFX); + + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + var groupPairs = DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).ToList(); + await Clients.Users(groupPairs).Client_GroupChangePermissions(new GroupPermissionDto(dto.Group, dto.Permissions)).ConfigureAwait(false); + } + + [Authorize(Policy = "Identified")] + public async Task GroupChangeIndividualPermissionState(GroupPairUserPermissionDto dto) + { + _logger.LogCallInfo(MareHubLogger.Args(dto)); + + var (inGroup, groupPair) = await TryValidateUserInGroup(dto.Group.GID).ConfigureAwait(false); + if (!inGroup) return; + + var wasPaused = groupPair.IsPaused; + groupPair.DisableSounds = dto.GroupPairPermissions.IsDisableSounds(); + groupPair.DisableAnimations = dto.GroupPairPermissions.IsDisableAnimations(); + groupPair.IsPaused = dto.GroupPairPermissions.IsPaused(); + groupPair.DisableVFX = dto.GroupPairPermissions.IsDisableVFX(); + + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + var groupPairs = DbContext.GroupPairs.Include(p => p.GroupUser).Where(p => p.GroupGID == dto.Group.GID).ToList(); + await Clients.Users(groupPairs.Select(p => p.GroupUserUID)).Client_GroupPairChangePermissions(dto).ConfigureAwait(false); + + var allUserPairs = await GetAllPairedClientsWithPauseState().ConfigureAwait(false); + var self = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); + + if (wasPaused == groupPair.IsPaused) return; + + foreach (var groupUserPair in groupPairs.Where(u => !string.Equals(u.GroupUserUID, UserUID, StringComparison.Ordinal)).ToList()) + { + var userPair = allUserPairs.SingleOrDefault(p => string.Equals(p.UID, groupUserPair.GroupUserUID, StringComparison.Ordinal)); + if (userPair != null) + { + if (userPair.IsDirectlyPaused != PauseInfo.NoConnection) continue; + if (userPair.IsPausedExcludingGroup(dto.Group.GID) is PauseInfo.Unpaused) continue; + if (userPair.IsOtherPausedForSpecificGroup(dto.Group.GID) is PauseInfo.Paused) continue; + } + + var groupUserIdent = await GetUserIdent(groupUserPair.GroupUserUID).ConfigureAwait(false); + if (!string.IsNullOrEmpty(groupUserIdent)) + { + if (!groupPair.IsPaused) + { + await Clients.User(UserUID).Client_UserSendOnline(new(groupUserPair.ToUserData(), groupUserIdent)).ConfigureAwait(false); + await Clients.User(groupUserPair.GroupUserUID) + .Client_UserSendOnline(new(self.ToUserData(), UserCharaIdent)).ConfigureAwait(false); + } + else + { + await Clients.User(UserUID).Client_UserSendOffline(new(groupUserPair.ToUserData())).ConfigureAwait(false); + await Clients.User(groupUserPair.GroupUserUID) + .Client_UserSendOffline(new(self.ToUserData())).ConfigureAwait(false); + } + } + } + } + + [Authorize(Policy = "Identified")] + public async Task GroupChangeOwnership(GroupPairDto dto) + { + _logger.LogCallInfo(MareHubLogger.Args(dto)); + + var (isOwner, group) = await TryValidateOwner(dto.Group.GID).ConfigureAwait(false); + if (!isOwner) return; + + var (isInGroup, newOwnerPair) = await TryValidateUserInGroup(dto.Group.GID, dto.User.UID).ConfigureAwait(false); + if (!isInGroup) return; + + var ownedShells = await DbContext.Groups.CountAsync(g => g.OwnerUID == dto.User.UID).ConfigureAwait(false); + if (ownedShells >= _maxExistingGroupsByUser) return; + + var prevOwner = await DbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == dto.Group.GID && g.GroupUserUID == UserUID).ConfigureAwait(false); + prevOwner.IsPinned = false; + group.Owner = newOwnerPair.GroupUser; + group.Alias = null; + newOwnerPair.IsPinned = true; + newOwnerPair.IsModerator = false; + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + _logger.LogCallInfo(MareHubLogger.Args(dto, "Success")); + + var groupPairs = await DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).AsNoTracking().ToListAsync().ConfigureAwait(false); + + await Clients.Users(groupPairs).Client_GroupSendInfo(new GroupInfoDto(group.ToGroupData(), newOwnerPair.GroupUser.ToUserData(), group.GetGroupPermissions())).ConfigureAwait(false); + } + + [Authorize(Policy = "Identified")] + public async Task GroupChangePassword(GroupPasswordDto dto) + { + _logger.LogCallInfo(MareHubLogger.Args(dto)); + + var (isOwner, group) = await TryValidateOwner(dto.Group.GID).ConfigureAwait(false); + if (!isOwner || dto.Password.Length < 10) return false; + + _logger.LogCallInfo(MareHubLogger.Args(dto, "Success")); + + group.HashedPassword = StringUtils.Sha256String(dto.Password); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + return true; + } + + [Authorize(Policy = "Identified")] + public async Task GroupClear(GroupDto dto) + { + _logger.LogCallInfo(MareHubLogger.Args(dto)); + + var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false); + if (!hasRights) return; + + var groupPairs = await DbContext.GroupPairs.Include(p => p.GroupUser).Where(p => p.GroupGID == dto.Group.GID).ToListAsync().ConfigureAwait(false); + + await Clients.Users(groupPairs.Where(p => !p.IsPinned && !p.IsModerator).Select(g => g.GroupUserUID)).Client_GroupDelete(new GroupDto(group.ToGroupData())).ConfigureAwait(false); + + _logger.LogCallInfo(MareHubLogger.Args(dto, "Success")); + + var notPinned = groupPairs.Where(g => !g.IsPinned && !g.IsModerator).ToList(); + + DbContext.GroupPairs.RemoveRange(notPinned); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + foreach (var pair in notPinned) + { + await Clients.Users(groupPairs.Where(p => p.IsPinned).Select(g => g.GroupUserUID)).Client_GroupPairLeft(new GroupPairDto(dto.Group, pair.GroupUser.ToUserData())).ConfigureAwait(false); + + var pairIdent = await GetUserIdent(pair.GroupUserUID).ConfigureAwait(false); + if (string.IsNullOrEmpty(pairIdent)) continue; + + var allUserPairs = await GetAllPairedClientsWithPauseState(pair.GroupUserUID).ConfigureAwait(false); + + var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == pair.GroupUserUID).ToListAsync().ConfigureAwait(false); + DbContext.CharaDataAllowances.RemoveRange(sharedData); + + foreach (var groupUserPair in groupPairs.Where(p => !string.Equals(p.GroupUserUID, pair.GroupUserUID, StringComparison.Ordinal))) + { + await UserGroupLeave(groupUserPair, allUserPairs, pairIdent).ConfigureAwait(false); + } + } + } + + [Authorize(Policy = "Identified")] + public async Task GroupCreate() + { + _logger.LogCallInfo(); + var existingGroupsByUser = await DbContext.Groups.CountAsync(u => u.OwnerUID == UserUID).ConfigureAwait(false); + var existingJoinedGroups = await DbContext.GroupPairs.CountAsync(u => u.GroupUserUID == UserUID).ConfigureAwait(false); + if (existingGroupsByUser >= _maxExistingGroupsByUser || existingJoinedGroups >= _maxJoinedGroupsByUser) + { + throw new System.Exception($"Max groups for user is {_maxExistingGroupsByUser}, max joined groups is {_maxJoinedGroupsByUser}."); + } + + var gid = StringUtils.GenerateRandomString(9); + while (await DbContext.Groups.AnyAsync(g => g.GID == "SNOW-" + gid).ConfigureAwait(false)) + { + gid = StringUtils.GenerateRandomString(9); + } + gid = "SNOW-" + gid; + + var passwd = StringUtils.GenerateRandomString(16); + var sha = SHA256.Create(); + var hashedPw = StringUtils.Sha256String(passwd); + + Group newGroup = new() + { + GID = gid, + HashedPassword = hashedPw, + InvitesEnabled = true, + OwnerUID = UserUID, + }; + + GroupPair initialPair = new() + { + GroupGID = newGroup.GID, + GroupUserUID = UserUID, + IsPaused = false, + IsPinned = true, + }; + + await DbContext.Groups.AddAsync(newGroup).ConfigureAwait(false); + await DbContext.GroupPairs.AddAsync(initialPair).ConfigureAwait(false); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + var self = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); + + await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(newGroup.ToGroupData(), self.ToUserData(), GroupPermissions.NoneSet, GroupUserPermissions.NoneSet, GroupUserInfo.None)) + .ConfigureAwait(false); + + _logger.LogCallInfo(MareHubLogger.Args(gid)); + + return new GroupPasswordDto(newGroup.ToGroupData(), passwd); + } + + [Authorize(Policy = "Identified")] + public async Task> GroupCreateTempInvite(GroupDto dto, int amount) + { + _logger.LogCallInfo(MareHubLogger.Args(dto, amount)); + List inviteCodes = new(); + List tempInvites = new(); + var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false); + if (!hasRights) return new(); + + var existingInvites = await DbContext.GroupTempInvites.Where(g => g.GroupGID == group.GID).ToListAsync().ConfigureAwait(false); + + for (int i = 0; i < amount; i++) + { + bool hasValidInvite = false; + string invite = string.Empty; + string hashedInvite = string.Empty; + while (!hasValidInvite) + { + invite = StringUtils.GenerateRandomString(10, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"); + hashedInvite = StringUtils.Sha256String(invite); + if (existingInvites.Any(i => string.Equals(i.Invite, hashedInvite, StringComparison.Ordinal))) continue; + hasValidInvite = true; + inviteCodes.Add(invite); + } + + tempInvites.Add(new GroupTempInvite() + { + ExpirationDate = DateTime.UtcNow.AddDays(1), + GroupGID = group.GID, + Invite = hashedInvite, + }); + } + + DbContext.GroupTempInvites.AddRange(tempInvites); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + return inviteCodes; + } + + [Authorize(Policy = "Identified")] + public async Task GroupDelete(GroupDto dto) + { + _logger.LogCallInfo(MareHubLogger.Args(dto)); + + var (hasRights, group) = await TryValidateOwner(dto.Group.GID).ConfigureAwait(false); + + _logger.LogCallInfo(MareHubLogger.Args(dto, "Success")); + + var groupPairs = await DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).ToListAsync().ConfigureAwait(false); + DbContext.RemoveRange(groupPairs); + DbContext.Remove(group); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + await Clients.Users(groupPairs.Select(g => g.GroupUserUID)).Client_GroupDelete(new GroupDto(group.ToGroupData())).ConfigureAwait(false); + + await SendGroupDeletedToAll(groupPairs).ConfigureAwait(false); + } + + [Authorize(Policy = "Identified")] + public async Task> GroupGetBannedUsers(GroupDto dto) + { + _logger.LogCallInfo(MareHubLogger.Args(dto)); + + var (userHasRights, group) = await TryValidateGroupModeratorOrOwner(dto.GID).ConfigureAwait(false); + if (!userHasRights) return new List(); + + var banEntries = await DbContext.GroupBans.Include(b => b.BannedUser).Where(g => g.GroupGID == dto.Group.GID).AsNoTracking().ToListAsync().ConfigureAwait(false); + + List bannedGroupUsers = banEntries.Select(b => + new BannedGroupUserDto(group.ToGroupData(), b.BannedUser.ToUserData(), b.BannedReason, b.BannedOn, + b.BannedByUID)).ToList(); + + _logger.LogCallInfo(MareHubLogger.Args(dto, bannedGroupUsers.Count)); + + return bannedGroupUsers; + } + + [Authorize(Policy = "Identified")] + public async Task GroupJoin(GroupPasswordDto dto) + { + var aliasOrGid = dto.Group.GID.Trim(); + + _logger.LogCallInfo(MareHubLogger.Args(dto.Group)); + + var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid).ConfigureAwait(false); + var groupGid = group?.GID ?? string.Empty; + var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID).ConfigureAwait(false); + var hashedPw = StringUtils.Sha256String(dto.Password); + var existingUserCount = await DbContext.GroupPairs.AsNoTracking().CountAsync(g => g.GroupGID == groupGid).ConfigureAwait(false); + var joinedGroups = await DbContext.GroupPairs.CountAsync(g => g.GroupUserUID == UserUID).ConfigureAwait(false); + var isBanned = await DbContext.GroupBans.AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID).ConfigureAwait(false); + var oneTimeInvite = await DbContext.GroupTempInvites.SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == hashedPw).ConfigureAwait(false); + + if (group == null + || (!string.Equals(group.HashedPassword, hashedPw, StringComparison.Ordinal) && oneTimeInvite == null) + || existingPair != null + || existingUserCount >= _maxGroupUserCount + || !group.InvitesEnabled + || joinedGroups >= _maxJoinedGroupsByUser + || isBanned) + return false; + + if (oneTimeInvite != null) + { + _logger.LogCallInfo(MareHubLogger.Args(aliasOrGid, "TempInvite", oneTimeInvite.Invite)); + DbContext.Remove(oneTimeInvite); + } + + GroupPair newPair = new() + { + GroupGID = group.GID, + GroupUserUID = UserUID, + DisableAnimations = false, + DisableSounds = false, + DisableVFX = false + }; + + await DbContext.GroupPairs.AddAsync(newPair).ConfigureAwait(false); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + _logger.LogCallInfo(MareHubLogger.Args(aliasOrGid, "Success")); + + await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(group.ToGroupData(), group.Owner.ToUserData(), group.GetGroupPermissions(), newPair.GetGroupPairPermissions(), newPair.GetGroupPairUserInfo())).ConfigureAwait(false); + + var self = DbContext.Users.Single(u => u.UID == UserUID); + + var groupPairs = await DbContext.GroupPairs.Include(p => p.GroupUser).Where(p => p.GroupGID == group.GID && p.GroupUserUID != UserUID).ToListAsync().ConfigureAwait(false); + + await Clients.Users(groupPairs.Select(p => p.GroupUserUID)) + .Client_GroupPairJoined(new GroupPairFullInfoDto(group.ToGroupData(), self.ToUserData(), newPair.GetGroupPairUserInfo(), newPair.GetGroupPairPermissions())).ConfigureAwait(false); + foreach (var pair in groupPairs) + { + await Clients.User(UserUID).Client_GroupPairJoined(new GroupPairFullInfoDto(group.ToGroupData(), pair.ToUserData(), pair.GetGroupPairUserInfo(), pair.GetGroupPairPermissions())).ConfigureAwait(false); + } + + var allUserPairs = await GetAllPairedClientsWithPauseState().ConfigureAwait(false); + + foreach (var groupUserPair in groupPairs) + { + var userPair = allUserPairs.Single(p => string.Equals(p.UID, groupUserPair.GroupUserUID, StringComparison.Ordinal)); + if (userPair.IsDirectlyPaused != PauseInfo.NoConnection) continue; + if (userPair.IsPausedExcludingGroup(group.GID) is PauseInfo.Unpaused) continue; + if (userPair.IsPausedPerGroup is PauseInfo.Paused) continue; + + var groupUserIdent = await GetUserIdent(groupUserPair.GroupUserUID).ConfigureAwait(false); + if (!string.IsNullOrEmpty(groupUserIdent)) + { + await Clients.User(UserUID).Client_UserSendOnline(new(groupUserPair.ToUserData(), groupUserIdent)).ConfigureAwait(false); + await Clients.User(groupUserPair.GroupUserUID) + .Client_UserSendOnline(new(self.ToUserData(), UserCharaIdent)).ConfigureAwait(false); + } + } + + return true; + } + + [Authorize(Policy = "Identified")] + public async Task GroupLeave(GroupDto dto) + { + await UserLeaveGroup(dto, UserUID).ConfigureAwait(false); + } + + [Authorize(Policy = "Identified")] + public async Task GroupPrune(GroupDto dto, int days, bool execute) + { + _logger.LogCallInfo(MareHubLogger.Args(dto, days, execute)); + + var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false); + if (!hasRights) return -1; + + var allGroupUsers = await DbContext.GroupPairs.Include(p => p.GroupUser).Include(p => p.Group) + .Where(g => g.GroupGID == dto.Group.GID) + .ToListAsync().ConfigureAwait(false); + var usersToPrune = allGroupUsers.Where(p => !p.IsPinned && !p.IsModerator + && p.GroupUserUID != UserUID + && p.Group.OwnerUID != p.GroupUserUID + && p.GroupUser.LastLoggedIn.AddDays(days) < DateTime.UtcNow); + + if (!execute) return usersToPrune.Count(); + + DbContext.GroupPairs.RemoveRange(usersToPrune); + + foreach (var pair in usersToPrune) + { + await Clients.Users(allGroupUsers.Where(p => !usersToPrune.Contains(p)).Select(g => g.GroupUserUID)) + .Client_GroupPairLeft(new GroupPairDto(dto.Group, pair.GroupUser.ToUserData())).ConfigureAwait(false); + } + + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + return usersToPrune.Count(); + } + + [Authorize(Policy = "Identified")] + public async Task GroupRemoveUser(GroupPairDto dto) + { + _logger.LogCallInfo(MareHubLogger.Args(dto)); + + var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false); + if (!hasRights) return; + + var (userExists, groupPair) = await TryValidateUserInGroup(dto.Group.GID, dto.User.UID).ConfigureAwait(false); + if (!userExists) return; + + if (groupPair.IsModerator || string.Equals(group.OwnerUID, dto.User.UID, StringComparison.Ordinal)) return; + _logger.LogCallInfo(MareHubLogger.Args(dto, "Success")); + + DbContext.GroupPairs.Remove(groupPair); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + var groupPairs = DbContext.GroupPairs.Where(p => p.GroupGID == group.GID).AsNoTracking().ToList(); + await Clients.Users(groupPairs.Select(p => p.GroupUserUID)).Client_GroupPairLeft(dto).ConfigureAwait(false); + + var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == dto.UID).ToListAsync().ConfigureAwait(false); + DbContext.CharaDataAllowances.RemoveRange(sharedData); + + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + var userIdent = await GetUserIdent(dto.User.UID).ConfigureAwait(false); + if (userIdent == null) return; + + await Clients.User(dto.User.UID).Client_GroupDelete(new GroupDto(dto.Group)).ConfigureAwait(false); + + var allUserPairs = await GetAllPairedClientsWithPauseState(dto.User.UID).ConfigureAwait(false); + + foreach (var groupUserPair in groupPairs) + { + await UserGroupLeave(groupUserPair, allUserPairs, userIdent, dto.User.UID).ConfigureAwait(false); + } + } + + [Authorize(Policy = "Identified")] + public async Task GroupSetUserInfo(GroupPairUserInfoDto dto) + { + _logger.LogCallInfo(MareHubLogger.Args(dto)); + + var (userExists, userPair) = await TryValidateUserInGroup(dto.Group.GID, dto.User.UID).ConfigureAwait(false); + if (!userExists) return; + + var (userIsOwner, _) = await TryValidateOwner(dto.Group.GID).ConfigureAwait(false); + var (userIsModerator, _) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false); + + if (dto.GroupUserInfo.HasFlag(GroupUserInfo.IsPinned) && userIsModerator && !userPair.IsPinned) + { + userPair.IsPinned = true; + } + else if (userIsModerator && userPair.IsPinned) + { + userPair.IsPinned = false; + } + + if (dto.GroupUserInfo.HasFlag(GroupUserInfo.IsModerator) && userIsOwner && !userPair.IsModerator) + { + userPair.IsModerator = true; + } + else if (userIsOwner && userPair.IsModerator) + { + userPair.IsModerator = false; + } + + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + var groupPairs = await DbContext.GroupPairs.AsNoTracking().Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).ToListAsync().ConfigureAwait(false); + await Clients.Users(groupPairs).Client_GroupPairChangeUserInfo(new GroupPairUserInfoDto(dto.Group, dto.User, userPair.GetGroupPairUserInfo())).ConfigureAwait(false); + } + + [Authorize(Policy = "Identified")] + public async Task> GroupsGetAll() + { + _logger.LogCallInfo(); + + var groups = await DbContext.GroupPairs.Include(g => g.Group).Include(g => g.Group.Owner).Where(g => g.GroupUserUID == UserUID).AsNoTracking().ToListAsync().ConfigureAwait(false); + + return groups.Select(g => new GroupFullInfoDto(g.Group.ToGroupData(), g.Group.Owner.ToUserData(), + g.Group.GetGroupPermissions(), g.GetGroupPairPermissions(), g.GetGroupPairUserInfo())).ToList(); + } + + [Authorize(Policy = "Identified")] + public async Task> GroupsGetUsersInGroup(GroupDto dto) + { + _logger.LogCallInfo(MareHubLogger.Args(dto)); + + var (inGroup, _) = await TryValidateUserInGroup(dto.Group.GID).ConfigureAwait(false); + if (!inGroup) return new List(); + + var group = await DbContext.Groups.SingleAsync(g => g.GID == dto.Group.GID).ConfigureAwait(false); + var allPairs = await DbContext.GroupPairs.Include(g => g.GroupUser).Where(g => g.GroupGID == dto.Group.GID && g.GroupUserUID != UserUID).AsNoTracking().ToListAsync().ConfigureAwait(false); + return allPairs.Select(p => new GroupPairFullInfoDto(group.ToGroupData(), p.GroupUser.ToUserData(), p.GetGroupPairUserInfo(), p.GetGroupPairPermissions())).ToList(); + } + + [Authorize(Policy = "Identified")] + public async Task GroupUnbanUser(GroupPairDto dto) + { + _logger.LogCallInfo(MareHubLogger.Args(dto)); + + var (userHasRights, _) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false); + if (!userHasRights) return; + + var banEntry = await DbContext.GroupBans.SingleOrDefaultAsync(g => g.GroupGID == dto.Group.GID && g.BannedUserUID == dto.User.UID).ConfigureAwait(false); + if (banEntry == null) return; + + DbContext.Remove(banEntry); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + _logger.LogCallInfo(MareHubLogger.Args(dto, "Success")); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.User.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.User.cs new file mode 100644 index 0000000..77bcd96 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.User.cs @@ -0,0 +1,514 @@ +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.API.Dto.User; +using MareSynchronosServer.Utils; +using MareSynchronosShared.Metrics; +using MareSynchronosShared.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace MareSynchronosServer.Hubs; + +public partial class MareHub +{ + private static readonly string[] AllowedExtensionsForGamePaths = { ".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk" }; + + [Authorize(Policy = "Identified")] + public async Task UserAddPair(UserDto dto) + { + _logger.LogCallInfo(MareHubLogger.Args(dto)); + + // don't allow adding nothing + var uid = dto.User.UID.Trim(); + if (string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal) || string.IsNullOrWhiteSpace(dto.User.UID)) return; + + // grab other user, check if it exists and if a pair already exists + var otherUser = await DbContext.Users.SingleOrDefaultAsync(u => u.UID == uid || u.Alias == uid).ConfigureAwait(false); + if (otherUser == null) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, $"Cannot pair with {dto.User.UID}, UID does not exist").ConfigureAwait(false); + return; + } + + if (string.Equals(otherUser.UID, UserUID, StringComparison.Ordinal)) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, $"My god you can't pair with yourself why would you do that please stop").ConfigureAwait(false); + return; + } + + var existingEntry = + await DbContext.ClientPairs.AsNoTracking() + .FirstOrDefaultAsync(p => + p.User.UID == UserUID && p.OtherUserUID == otherUser.UID).ConfigureAwait(false); + + if (existingEntry != null) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, $"Cannot pair with {dto.User.UID}, already paired").ConfigureAwait(false); + return; + } + + // grab self create new client pair and save + var user = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); + + _logger.LogCallInfo(MareHubLogger.Args(dto, "Success")); + + ClientPair wl = new ClientPair() + { + IsPaused = false, + OtherUser = otherUser, + User = user, + }; + await DbContext.ClientPairs.AddAsync(wl).ConfigureAwait(false); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + // get the opposite entry of the client pair + var otherEntry = OppositeEntry(otherUser.UID); + var otherIdent = await GetUserIdent(otherUser.UID).ConfigureAwait(false); + + var ownPerm = UserPermissions.Paired; + var otherPerm = UserPermissions.NoneSet; + otherPerm.SetPaired(otherEntry != null); + otherPerm.SetPaused(otherEntry?.IsPaused ?? false); + var userPairResponse = new UserPairDto(otherUser.ToUserData(), ownPerm, otherPerm); + await Clients.User(user.UID).Client_UserAddClientPair(userPairResponse).ConfigureAwait(false); + + // check if other user is online + if (otherIdent == null || otherEntry == null) return; + + // send push with update to other user if other user is online + await Clients.User(otherUser.UID).Client_UserAddClientPair(new UserPairDto(user.ToUserData(), otherPerm, ownPerm)).ConfigureAwait(false); + + if (!otherPerm.IsPaused()) + { + await Clients.User(UserUID).Client_UserSendOnline(new(otherUser.ToUserData(), otherIdent)).ConfigureAwait(false); + await Clients.User(otherUser.UID).Client_UserSendOnline(new(user.ToUserData(), UserCharaIdent)).ConfigureAwait(false); + } + } + + [Authorize(Policy = "Identified")] + public async Task UserDelete() + { + _logger.LogCallInfo(); + + var userEntry = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); + var secondaryUsers = await DbContext.Auth.Include(u => u.User).Where(u => u.PrimaryUserUID == UserUID).Select(c => c.User).ToListAsync().ConfigureAwait(false); + foreach (var user in secondaryUsers) + { + await DeleteUser(user).ConfigureAwait(false); + } + + await DeleteUser(userEntry).ConfigureAwait(false); + } + + [Authorize(Policy = "Identified")] + public async Task> UserGetOnlinePairs() + { + _logger.LogCallInfo(); + + var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false); + var pairs = await GetOnlineUsers(allPairedUsers).ConfigureAwait(false); + + await SendOnlineToAllPairedUsers().ConfigureAwait(false); + + return pairs.Select(p => new OnlineUserIdentDto(new UserData(p.Key), p.Value)).ToList(); + } + + [Authorize(Policy = "Identified")] + public async Task> UserGetPairedClients() + { + _logger.LogCallInfo(); + + var query = + from userToOther in DbContext.ClientPairs + join otherToUser in DbContext.ClientPairs + on new + { + user = userToOther.UserUID, + other = userToOther.OtherUserUID, + } equals new + { + user = otherToUser.OtherUserUID, + other = otherToUser.UserUID, + } into leftJoin + from otherEntry in leftJoin.DefaultIfEmpty() + where + userToOther.UserUID == UserUID + select new + { + userToOther.OtherUser.Alias, + userToOther.IsPaused, + OtherIsPaused = otherEntry != null && otherEntry.IsPaused, + userToOther.OtherUserUID, + IsSynced = otherEntry != null, + DisableOwnAnimations = userToOther.DisableAnimations, + DisableOwnSounds = userToOther.DisableSounds, + DisableOwnVFX = userToOther.DisableVFX, + DisableOtherAnimations = otherEntry == null ? false : otherEntry.DisableAnimations, + DisableOtherSounds = otherEntry == null ? false : otherEntry.DisableSounds, + DisableOtherVFX = otherEntry == null ? false : otherEntry.DisableVFX + }; + + var results = await query.AsNoTracking().ToListAsync().ConfigureAwait(false); + + return results.Select(c => + { + var ownPerm = UserPermissions.Paired; + ownPerm.SetPaused(c.IsPaused); + ownPerm.SetDisableAnimations(c.DisableOwnAnimations); + ownPerm.SetDisableSounds(c.DisableOwnSounds); + ownPerm.SetDisableVFX(c.DisableOwnVFX); + var otherPerm = UserPermissions.NoneSet; + otherPerm.SetPaired(c.IsSynced); + otherPerm.SetPaused(c.OtherIsPaused); + otherPerm.SetDisableAnimations(c.DisableOtherAnimations); + otherPerm.SetDisableSounds(c.DisableOtherSounds); + otherPerm.SetDisableVFX(c.DisableOtherVFX); + return new UserPairDto(new(c.OtherUserUID, c.Alias), ownPerm, otherPerm); + }).ToList(); + } + + [Authorize(Policy = "Identified")] + public async Task UserGetProfile(UserDto user) + { + _logger.LogCallInfo(MareHubLogger.Args(user)); + + var allUserPairs = await GetAllPairedUnpausedUsers().ConfigureAwait(false); + + if (!allUserPairs.Contains(user.User.UID, StringComparer.Ordinal) && !string.Equals(user.User.UID, UserUID, StringComparison.Ordinal)) + { + return new UserProfileDto(user.User, false, null, null, "Due to the pause status you cannot access this users profile."); + } + + var data = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == user.User.UID).ConfigureAwait(false); + if (data == null) return new UserProfileDto(user.User, false, null, null, null); + + if (data.FlaggedForReport) return new UserProfileDto(user.User, true, null, null, "This profile is flagged for report and pending evaluation"); + if (data.ProfileDisabled) return new UserProfileDto(user.User, true, null, null, "This profile was permanently disabled"); + + return new UserProfileDto(user.User, false, data.IsNSFW, data.Base64ProfileImage, data.UserDescription); + } + + [Authorize(Policy = "Identified")] + public async Task UserPushData(UserCharaDataMessageDto dto) + { + _logger.LogCallInfo(MareHubLogger.Args(dto.CharaData.FileReplacements.Count)); + + // check for honorific containing . and / + try + { + var honorificJson = Encoding.Default.GetString(Convert.FromBase64String(dto.CharaData.HonorificData)); + var deserialized = JsonSerializer.Deserialize(honorificJson); + if (deserialized.TryGetProperty("Title", out var honorificTitle)) + { + var title = honorificTitle.GetString().Normalize(NormalizationForm.FormKD); + if (UrlRegex().IsMatch(title)) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your data was not pushed: The usage of URLs the Honorific titles is prohibited. Remove them to be able to continue to push data.").ConfigureAwait(false); + throw new HubException("Invalid data provided, Honorific title invalid: " + title); + } + } + } + catch (HubException) + { + throw; + } + catch (Exception) + { + // swallow + } + + bool hadInvalidData = false; + List invalidGamePaths = new(); + List invalidFileSwapPaths = new(); + foreach (var replacement in dto.CharaData.FileReplacements.SelectMany(p => p.Value)) + { + var invalidPaths = replacement.GamePaths.Where(p => !GamePathRegex().IsMatch(p)).ToList(); + invalidPaths.AddRange(replacement.GamePaths.Where(p => !AllowedExtensionsForGamePaths.Any(e => p.EndsWith(e, StringComparison.OrdinalIgnoreCase)))); + replacement.GamePaths = replacement.GamePaths.Where(p => !invalidPaths.Contains(p, StringComparer.OrdinalIgnoreCase)).ToArray(); + bool validGamePaths = replacement.GamePaths.Any(); + bool validHash = string.IsNullOrEmpty(replacement.Hash) || HashRegex().IsMatch(replacement.Hash); + bool validFileSwapPath = string.IsNullOrEmpty(replacement.FileSwapPath) || GamePathRegex().IsMatch(replacement.FileSwapPath); + if (!validGamePaths || !validHash || !validFileSwapPath) + { + _logger.LogCallWarning(MareHubLogger.Args("Invalid Data", "GamePaths", validGamePaths, string.Join(",", invalidPaths), "Hash", validHash, replacement.Hash, "FileSwap", validFileSwapPath, replacement.FileSwapPath)); + hadInvalidData = true; + if (!validFileSwapPath) invalidFileSwapPaths.Add(replacement.FileSwapPath); + if (!validGamePaths) invalidGamePaths.AddRange(replacement.GamePaths); + if (!validHash) invalidFileSwapPaths.Add(replacement.Hash); + } + } + + if (hadInvalidData) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "One or more of your supplied mods were rejected from the server. Consult /xllog for more information.").ConfigureAwait(false); + throw new HubException("Invalid data provided, contact the appropriate mod creator to resolve those issues" + + Environment.NewLine + + string.Join(Environment.NewLine, invalidGamePaths.Select(p => "Invalid Game Path: " + p)) + + Environment.NewLine + + string.Join(Environment.NewLine, invalidFileSwapPaths.Select(p => "Invalid FileSwap Path: " + p))); + } + + var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false); + var idents = await GetOnlineUsers(allPairedUsers).ConfigureAwait(false); + + var recipients = allPairedUsers.Where(f => dto.Recipients.Select(r => r.UID).Contains(f, StringComparer.Ordinal)).ToList(); + + _logger.LogCallInfo(MareHubLogger.Args(idents.Count, recipients.Count())); + + await Clients.Users(recipients).Client_UserReceiveCharacterData(new OnlineUserCharaDataDto(new UserData(UserUID), dto.CharaData)).ConfigureAwait(false); + + _mareMetrics.IncCounter(MetricsAPI.CounterUserPushData); + _mareMetrics.IncCounter(MetricsAPI.CounterUserPushDataTo, recipients.Count()); + } + + [Authorize(Policy = "Identified")] + public async Task UserRemovePair(UserDto dto) + { + _logger.LogCallInfo(MareHubLogger.Args(dto)); + + if (string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal)) return; + + // check if client pair even exists + ClientPair callerPair = + await DbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == UserUID && w.OtherUserUID == dto.User.UID).ConfigureAwait(false); + if (callerPair == null) return; + + bool callerHadPaused = callerPair.IsPaused; + + // delete from database, send update info to users pair list + DbContext.ClientPairs.Remove(callerPair); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + _logger.LogCallInfo(MareHubLogger.Args(dto, "Success")); + + await Clients.User(UserUID).Client_UserRemoveClientPair(dto).ConfigureAwait(false); + + // check if opposite entry exists + var oppositeClientPair = OppositeEntry(dto.User.UID); + if (oppositeClientPair == null) return; + + // check if other user is online, if no then there is no need to do anything further + var otherIdent = await GetUserIdent(dto.User.UID).ConfigureAwait(false); + if (otherIdent == null) return; + + // get own ident and + await Clients.User(dto.User.UID) + .Client_UserUpdateOtherPairPermissions(new UserPermissionsDto(new UserData(UserUID), + UserPermissions.NoneSet)).ConfigureAwait(false); + + // if the other user had paused the user the state will be offline for either, do nothing + bool otherHadPaused = oppositeClientPair.IsPaused; + if (!callerHadPaused && otherHadPaused) return; + + var allUsers = await GetAllPairedClientsWithPauseState().ConfigureAwait(false); + var pauseEntry = allUsers.SingleOrDefault(f => string.Equals(f.UID, dto.User.UID, StringComparison.Ordinal)); + var isPausedInGroup = pauseEntry == null || pauseEntry.IsPausedPerGroup is PauseInfo.Paused or PauseInfo.NoConnection; + + // if neither user had paused each other and both are in unpaused groups, state will be online for both, do nothing + if (!callerHadPaused && !otherHadPaused && !isPausedInGroup) return; + + // if neither user had paused each other and either is not in an unpaused group with each other, change state to offline + if (!callerHadPaused && !otherHadPaused && isPausedInGroup) + { + await Clients.User(UserUID).Client_UserSendOffline(dto).ConfigureAwait(false); + await Clients.User(dto.User.UID).Client_UserSendOffline(new(new(UserUID))).ConfigureAwait(false); + } + + // if the caller had paused other but not the other has paused the caller and they are in an unpaused group together, change state to online + if (callerHadPaused && !otherHadPaused && !isPausedInGroup) + { + await Clients.User(UserUID).Client_UserSendOnline(new(dto.User, otherIdent)).ConfigureAwait(false); + await Clients.User(dto.User.UID).Client_UserSendOnline(new(new(UserUID), UserCharaIdent)).ConfigureAwait(false); + } + } + + [Authorize(Policy = "Identified")] + public async Task UserReportProfile(UserProfileReportDto dto) + { + _logger.LogCallInfo(MareHubLogger.Args(dto)); + + UserProfileDataReport report = await DbContext.UserProfileReports.SingleOrDefaultAsync(u => u.ReportedUserUID == dto.User.UID && u.ReportingUserUID == UserUID).ConfigureAwait(false); + if (report != null) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You already reported this profile and it's pending validation").ConfigureAwait(false); + return; + } + + UserProfileData profile = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == dto.User.UID).ConfigureAwait(false); + if (profile == null) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "This user has no profile").ConfigureAwait(false); + return; + } + + UserProfileDataReport reportToAdd = new() + { + ReportDate = DateTime.UtcNow, + ReportingUserUID = UserUID, + ReportReason = dto.ProfileReport, + ReportedUserUID = dto.User.UID, + }; + + profile.FlaggedForReport = true; + + await DbContext.UserProfileReports.AddAsync(reportToAdd).ConfigureAwait(false); + + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + var allPairedUsers = await GetAllPairedUnpausedUsers(dto.User.UID).ConfigureAwait(false); + var pairs = await GetOnlineUsers(allPairedUsers).ConfigureAwait(false); + + await Clients.Users(pairs.Select(p => p.Key)).Client_UserUpdateProfile(new(dto.User)).ConfigureAwait(false); + await Clients.Users(dto.User.UID).Client_UserUpdateProfile(new(dto.User)).ConfigureAwait(false); + } + + [Authorize(Policy = "Identified")] + public async Task UserSetPairPermissions(UserPermissionsDto dto) + { + _logger.LogCallInfo(MareHubLogger.Args(dto)); + + if (string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal)) return; + ClientPair pair = await DbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == UserUID && w.OtherUserUID == dto.User.UID).ConfigureAwait(false); + if (pair == null) return; + + var pauseChange = pair.IsPaused != dto.Permissions.IsPaused(); + + pair.IsPaused = dto.Permissions.IsPaused(); + pair.DisableAnimations = dto.Permissions.IsDisableAnimations(); + pair.DisableSounds = dto.Permissions.IsDisableSounds(); + pair.DisableVFX = dto.Permissions.IsDisableVFX(); + DbContext.Update(pair); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + _logger.LogCallInfo(MareHubLogger.Args(dto, "Success")); + + var otherEntry = OppositeEntry(dto.User.UID); + + await Clients.User(UserUID).Client_UserUpdateSelfPairPermissions(dto).ConfigureAwait(false); + + if (otherEntry != null) + { + await Clients.User(dto.User.UID).Client_UserUpdateOtherPairPermissions(new UserPermissionsDto(new UserData(UserUID), dto.Permissions)).ConfigureAwait(false); + + if (pauseChange) + { + var otherCharaIdent = await GetUserIdent(pair.OtherUserUID).ConfigureAwait(false); + + if (UserCharaIdent == null || otherCharaIdent == null || otherEntry.IsPaused) return; + + if (dto.Permissions.IsPaused()) + { + await Clients.User(UserUID).Client_UserSendOffline(dto).ConfigureAwait(false); + await Clients.User(dto.User.UID).Client_UserSendOffline(new(new(UserUID))).ConfigureAwait(false); + } + else + { + await Clients.User(UserUID).Client_UserSendOnline(new(dto.User, otherCharaIdent)).ConfigureAwait(false); + await Clients.User(dto.User.UID).Client_UserSendOnline(new(new(UserUID), UserCharaIdent)).ConfigureAwait(false); + } + } + } + } + + [Authorize(Policy = "Identified")] + public async Task UserSetProfile(UserProfileDto dto) + { + _logger.LogCallInfo(MareHubLogger.Args(dto)); + + if (!string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal)) throw new HubException("Cannot modify profile data for anyone but yourself"); + + var existingData = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == dto.User.UID).ConfigureAwait(false); + + if (existingData?.FlaggedForReport ?? false) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile is currently flagged for report and cannot be edited").ConfigureAwait(false); + return; + } + + if (existingData?.ProfileDisabled ?? false) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile was permanently disabled and cannot be edited").ConfigureAwait(false); + return; + } + + if (!string.IsNullOrEmpty(dto.ProfilePictureBase64)) + { + byte[] imageData = Convert.FromBase64String(dto.ProfilePictureBase64); + using MemoryStream ms = new(imageData); + var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false); + if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase)) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is not in PNG format").ConfigureAwait(false); + return; + } + using var image = Image.Load(imageData); + + if (image.Width > 256 || image.Height > 256 || (imageData.Length > 250 * 1024)) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is larger than 256x256 or more than 250KiB.").ConfigureAwait(false); + return; + } + } + + if (existingData != null) + { + if (string.Equals("", dto.ProfilePictureBase64, StringComparison.OrdinalIgnoreCase)) + { + existingData.Base64ProfileImage = null; + } + else if (dto.ProfilePictureBase64 != null) + { + existingData.Base64ProfileImage = dto.ProfilePictureBase64; + } + + if (dto.IsNSFW != null) + { + existingData.IsNSFW = dto.IsNSFW.Value; + } + + if (dto.Description != null) + { + existingData.UserDescription = dto.Description; + } + } + else + { + UserProfileData userProfileData = new() + { + UserUID = dto.User.UID, + Base64ProfileImage = dto.ProfilePictureBase64 ?? null, + UserDescription = dto.Description ?? null, + IsNSFW = dto.IsNSFW ?? false + }; + + await DbContext.UserProfileData.AddAsync(userProfileData).ConfigureAwait(false); + } + + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false); + var pairs = await GetOnlineUsers(allPairedUsers).ConfigureAwait(false); + + await Clients.Users(pairs.Select(p => p.Key)).Client_UserUpdateProfile(new(dto.User)).ConfigureAwait(false); + await Clients.Caller.Client_UserUpdateProfile(new(dto.User)).ConfigureAwait(false); + } + + [GeneratedRegex(@"^([a-z0-9_ '+&,\.\-\{\}]+\/)+([a-z0-9_ '+&,\.\-\{\}]+\.[a-z]{3,4})$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ECMAScript)] + private static partial Regex GamePathRegex(); + + [GeneratedRegex(@"^[A-Z0-9]{40}$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ECMAScript)] + private static partial Regex HashRegex(); + + [GeneratedRegex("^[-a-zA-Z0-9@:%._\\+~#=]{1,256}[\\.,][a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&\\/=]*)$")] + private static partial Regex UrlRegex(); + + private ClientPair OppositeEntry(string otherUID) => + DbContext.ClientPairs.AsNoTracking().SingleOrDefault(w => w.User.UID == otherUID && w.OtherUser.UID == UserUID); +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.cs new file mode 100644 index 0000000..c55a708 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.cs @@ -0,0 +1,147 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Dto; +using MareSynchronos.API.SignalR; +using MareSynchronosServer.Services; +using MareSynchronosServer.Utils; +using MareSynchronosShared; +using MareSynchronosShared.Data; +using MareSynchronosShared.Metrics; +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils.Configuration; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using StackExchange.Redis.Extensions.Core.Abstractions; + +namespace MareSynchronosServer.Hubs; + +[Authorize(Policy = "Authenticated")] +public partial class MareHub : Hub, IMareHub +{ + private readonly MareMetrics _mareMetrics; + private readonly SystemInfoService _systemInfoService; + private readonly IHttpContextAccessor _contextAccessor; + private readonly MareHubLogger _logger; + private readonly string _shardName; + private readonly int _maxExistingGroupsByUser; + private readonly int _maxJoinedGroupsByUser; + private readonly int _maxGroupUserCount; + private readonly IRedisDatabase _redis; + private readonly GPoseLobbyDistributionService _gPoseLobbyDistributionService; + private readonly Uri _fileServerAddress; + private readonly Version _expectedClientVersion; + private readonly int _maxCharaDataByUser; + + private readonly Lazy _dbContextLazy; + private MareDbContext DbContext => _dbContextLazy.Value; + + public MareHub(MareMetrics mareMetrics, + IDbContextFactory mareDbContextFactory, ILogger logger, SystemInfoService systemInfoService, + IConfigurationService configuration, IHttpContextAccessor contextAccessor, + IRedisDatabase redisDb, GPoseLobbyDistributionService gPoseLobbyDistributionService) + { + _mareMetrics = mareMetrics; + _systemInfoService = systemInfoService; + _shardName = configuration.GetValue(nameof(ServerConfiguration.ShardName)); + _maxExistingGroupsByUser = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxExistingGroupsByUser), 3); + _maxJoinedGroupsByUser = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxJoinedGroupsByUser), 6); + _maxGroupUserCount = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxGroupUserCount), 100); + _fileServerAddress = configuration.GetValue(nameof(ServerConfiguration.CdnFullUrl)); + _expectedClientVersion = configuration.GetValueOrDefault(nameof(ServerConfiguration.ExpectedClientVersion), new Version(0, 0, 0)); + _maxCharaDataByUser = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxCharaDataByUser), 10); + _contextAccessor = contextAccessor; + _redis = redisDb; + _gPoseLobbyDistributionService = gPoseLobbyDistributionService; + _logger = new MareHubLogger(this, logger); + _dbContextLazy = new Lazy(() => mareDbContextFactory.CreateDbContext()); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + DbContext.Dispose(); + } + + base.Dispose(disposing); + } + + [Authorize(Policy = "Identified")] + public async Task GetConnectionDto() + { + _logger.LogCallInfo(); + + _mareMetrics.IncCounter(MetricsAPI.CounterInitializedConnections); + + await Clients.Caller.Client_UpdateSystemInfo(_systemInfoService.SystemInfoDto).ConfigureAwait(false); + + var dbUser = DbContext.Users.SingleOrDefault(f => f.UID == UserUID); + dbUser.LastLoggedIn = DateTime.UtcNow; + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, "Welcome to Snowcloak! Current Online Users: " + _systemInfoService.SystemInfoDto.OnlineUsers).ConfigureAwait(false); + + return new ConnectionDto(new UserData(dbUser.UID, string.IsNullOrWhiteSpace(dbUser.Alias) ? null : dbUser.Alias)) + { + CurrentClientVersion = _expectedClientVersion, + ServerVersion = IMareHub.ApiVersion, + IsAdmin = dbUser.IsAdmin, + IsModerator = dbUser.IsModerator, + ServerInfo = new ServerInfo() + { + MaxGroupsCreatedByUser = _maxExistingGroupsByUser, + ShardName = _shardName, + MaxGroupsJoinedByUser = _maxJoinedGroupsByUser, + MaxGroupUserCount = _maxGroupUserCount, + FileServerAddress = _fileServerAddress, + MaxCharaData = _maxCharaDataByUser + }, + }; + } + + [Authorize(Policy = "Authenticated")] + public async Task CheckClientHealth() + { + await UpdateUserOnRedis().ConfigureAwait(false); + + return false; + } + + [Authorize(Policy = "Authenticated")] + public override async Task OnConnectedAsync() + { + _mareMetrics.IncGaugeWithLabels(MetricsAPI.GaugeConnections, labels: Continent); + + try + { + _logger.LogCallInfo(MareHubLogger.Args(_contextAccessor.GetIpAddress(), UserCharaIdent)); + + await UpdateUserOnRedis().ConfigureAwait(false); + } + catch { } + + await base.OnConnectedAsync().ConfigureAwait(false); + } + + [Authorize(Policy = "Authenticated")] + public override async Task OnDisconnectedAsync(Exception exception) + { + _mareMetrics.DecGaugeWithLabels(MetricsAPI.GaugeConnections, labels: Continent); + + try + { + _logger.LogCallInfo(MareHubLogger.Args(_contextAccessor.GetIpAddress(), UserCharaIdent)); + if (exception != null) + _logger.LogCallWarning(MareHubLogger.Args(_contextAccessor.GetIpAddress(), exception.Message, exception.StackTrace)); + + await GposeLobbyLeave().ConfigureAwait(false); + await RemoveUserFromRedis().ConfigureAwait(false); + + await SendOfflineToAllPairedUsers().ConfigureAwait(false); + } + catch { } + + await base.OnDisconnectedAsync(exception).ConfigureAwait(false); + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/SignalRLimitFilter.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/SignalRLimitFilter.cs new file mode 100644 index 0000000..a2e8017 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/SignalRLimitFilter.cs @@ -0,0 +1,112 @@ +using AspNetCoreRateLimit; +using MareSynchronosShared; +using MareSynchronosShared.Utils; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Options; + +namespace MareSynchronosServer.Hubs; +public class SignalRLimitFilter : IHubFilter +{ + private readonly IRateLimitProcessor _processor; + private readonly IHttpContextAccessor accessor; + private readonly ILogger logger; + private static readonly SemaphoreSlim ConnectionLimiterSemaphore = new(20, 20); + private static readonly SemaphoreSlim DisconnectLimiterSemaphore = new(20, 20); + + public SignalRLimitFilter( + IOptions options, IProcessingStrategy processing, IIpPolicyStore policyStore, IHttpContextAccessor accessor, ILogger logger) + { + _processor = new IpRateLimitProcessor(options?.Value, policyStore, processing); + this.accessor = accessor; + this.logger = logger; + } + + public async ValueTask InvokeMethodAsync( + HubInvocationContext invocationContext, Func> next) + { + var ip = accessor.GetIpAddress(); + var client = new ClientRequestIdentity + { + ClientIp = ip, + Path = invocationContext.HubMethodName, + HttpVerb = "ws", + ClientId = invocationContext.Context.UserIdentifier, + }; + foreach (var rule in await _processor.GetMatchingRulesAsync(client).ConfigureAwait(false)) + { + var counter = await _processor.ProcessRequestAsync(client, rule).ConfigureAwait(false); + if (counter.Count > rule.Limit) + { + var authUserId = invocationContext.Context.User.Claims?.SingleOrDefault(c => string.Equals(c.Type, MareClaimTypes.Uid, StringComparison.Ordinal))?.Value ?? "Unknown"; + var retry = counter.Timestamp.RetryAfterFrom(rule); + logger.LogWarning("Method rate limit triggered from {ip}/{authUserId}: {method}", ip, authUserId, invocationContext.HubMethodName); + throw new HubException($"call limit {retry}"); + } + } + + return await next(invocationContext).ConfigureAwait(false); + } + + // Optional method + public async Task OnConnectedAsync(HubLifetimeContext context, Func next) + { + await ConnectionLimiterSemaphore.WaitAsync().ConfigureAwait(false); + try + { + var ip = accessor.GetIpAddress(); + var client = new ClientRequestIdentity + { + ClientIp = ip, + Path = "Connect", + HttpVerb = "ws", + }; + foreach (var rule in await _processor.GetMatchingRulesAsync(client).ConfigureAwait(false)) + { + var counter = await _processor.ProcessRequestAsync(client, rule).ConfigureAwait(false); + if (counter.Count > rule.Limit) + { + var retry = counter.Timestamp.RetryAfterFrom(rule); + logger.LogWarning("Connection rate limit triggered from {ip}", ip); + ConnectionLimiterSemaphore.Release(); + throw new HubException($"Connection rate limit {retry}"); + } + } + + + await Task.Delay(25).ConfigureAwait(false); + await next(context).ConfigureAwait(false); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Error on OnConnectedAsync"); + } + finally + { + ConnectionLimiterSemaphore.Release(); + } + } + + public async Task OnDisconnectedAsync( + HubLifetimeContext context, Exception exception, Func next) + { + await DisconnectLimiterSemaphore.WaitAsync().ConfigureAwait(false); + if (exception != null) + { + logger.LogWarning(exception, "InitialException on OnDisconnectedAsync"); + } + + try + { + await next(context, exception).ConfigureAwait(false); + await Task.Delay(25).ConfigureAwait(false); + } + catch (Exception e) + { + logger.LogWarning(e, "ThrownException on OnDisconnectedAsync"); + } + finally + { + DisconnectLimiterSemaphore.Release(); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj b/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj new file mode 100644 index 0000000..b662f74 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj @@ -0,0 +1,42 @@ + + + + net8.0 + aspnet-MareSynchronosServer-BA82A12A-0B30-463C-801D-B7E81318CD50 + 1.1.0.0 + enable + + + + + + + + + + + Never + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/MareSynchronosServer/MareSynchronosServer/Program.cs b/MareSynchronosServer/MareSynchronosServer/Program.cs new file mode 100644 index 0000000..e05f94f --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Program.cs @@ -0,0 +1,77 @@ +using Microsoft.EntityFrameworkCore; +using MareSynchronosShared.Data; +using MareSynchronosShared.Metrics; +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils.Configuration; + +namespace MareSynchronosServer; + +public class Program +{ + public static void Main(string[] args) + { + var hostBuilder = CreateHostBuilder(args); + var host = hostBuilder.Build(); + using (var scope = host.Services.CreateScope()) + { + var services = scope.ServiceProvider; + using var context = services.GetRequiredService(); + var options = services.GetRequiredService>(); + var logger = host.Services.GetRequiredService>(); + + if (options.IsMain) + { + context.Database.Migrate(); + context.SaveChanges(); + + // clean up residuals + var unfinishedRegistrations = context.LodeStoneAuth.Where(c => c.StartedAt != null); + context.RemoveRange(unfinishedRegistrations); + context.SaveChanges(); + + logger.LogInformation(options.ToString()); + } + + var metrics = services.GetRequiredService(); + + metrics.SetGaugeTo(MetricsAPI.GaugeUsersRegistered, context.Users.AsNoTracking().Count()); + metrics.SetGaugeTo(MetricsAPI.GaugePairs, context.ClientPairs.AsNoTracking().Count()); + metrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, context.ClientPairs.AsNoTracking().Count(p => p.IsPaused)); + + } + + if (args.Length == 0 || !string.Equals(args[0], "dry", StringComparison.Ordinal)) + { + try + { + host.Run(); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + } + } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + var loggerFactory = LoggerFactory.Create(builder => + { + builder.ClearProviders(); + builder.AddConsole(); + }); + var logger = loggerFactory.CreateLogger(); + return Host.CreateDefaultBuilder(args) + .UseSystemd() + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseContentRoot(AppContext.BaseDirectory); + webBuilder.ConfigureLogging((ctx, builder) => + { + builder.AddConfiguration(ctx.Configuration.GetSection("Logging")); + builder.AddFile(o => o.RootPath = AppContext.BaseDirectory); + }); + webBuilder.UseStartup(ctx => new Startup(ctx.Configuration, logger)); + }); + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Properties/launchSettings.json b/MareSynchronosServer/MareSynchronosServer/Properties/launchSettings.json new file mode 100644 index 0000000..50deb2c --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "profiles": { + "MareSynchronosServer": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": false, + //"applicationUrl": "https://localhost:5001;http://localhost:5000;https://192.168.1.124:5001;http://192.168.1.124:5000", + "applicationUrl": "http://localhost:5000;https://localhost:5001;https://darkarchon.internet-box.ch:5001", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Properties/serviceDependencies.json b/MareSynchronosServer/MareSynchronosServer/Properties/serviceDependencies.json new file mode 100644 index 0000000..33703d5 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Properties/serviceDependencies.json @@ -0,0 +1,3 @@ +{ + "dependencies": {} +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServer/Properties/serviceDependencies.local.json b/MareSynchronosServer/MareSynchronosServer/Properties/serviceDependencies.local.json new file mode 100644 index 0000000..33703d5 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Properties/serviceDependencies.local.json @@ -0,0 +1,3 @@ +{ + "dependencies": {} +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServer/Services/CharaDataCleanupService.cs b/MareSynchronosServer/MareSynchronosServer/Services/CharaDataCleanupService.cs new file mode 100644 index 0000000..fb0735b --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Services/CharaDataCleanupService.cs @@ -0,0 +1,50 @@ +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; + +namespace MareSynchronosServer.Services; + +public class CharaDataCleanupService : IHostedService +{ + private readonly ILogger _logger; + private readonly IDbContextFactory _dbContextFactory; + private readonly CancellationTokenSource _cleanupCts = new(); + + public CharaDataCleanupService(ILogger logger, IDbContextFactory dbContextFactory) + { + _logger = logger; + _dbContextFactory = dbContextFactory; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _ = Cleanup(cancellationToken); + return Task.CompletedTask; + } + + private async Task Cleanup(CancellationToken ct) + { + _logger.LogInformation("CharaData Cleanup Service started"); + while (!ct.IsCancellationRequested) + { + using (var db = await _dbContextFactory.CreateDbContextAsync(ct).ConfigureAwait(false)) + { + var dateTime = DateTime.UtcNow; + var expiredData = await db.CharaData.Where(c => c.ExpiryDate <= DateTime.UtcNow).ToListAsync(cancellationToken: ct).ConfigureAwait(false); + + _logger.LogInformation("Removing {count} expired Chara Data entries", expiredData.Count); + + db.RemoveRange(expiredData); + await db.SaveChangesAsync(ct).ConfigureAwait(false); + } + + await Task.Delay(TimeSpan.FromHours(12), ct).ConfigureAwait(false); + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _cleanupCts?.Cancel(); + _cleanupCts?.Dispose(); + return Task.CompletedTask; + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Services/GPoseLobbyDistributionService.cs b/MareSynchronosServer/MareSynchronosServer/Services/GPoseLobbyDistributionService.cs new file mode 100644 index 0000000..249407d --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Services/GPoseLobbyDistributionService.cs @@ -0,0 +1,226 @@ +using MareSynchronos.API.Dto.CharaData; +using MareSynchronos.API.SignalR; +using MareSynchronosServer.Hubs; +using Microsoft.AspNetCore.SignalR; +using StackExchange.Redis.Extensions.Core.Abstractions; + +namespace MareSynchronosServer.Services; + +public sealed class GPoseLobbyDistributionService : IHostedService, IDisposable +{ + private CancellationTokenSource _runtimeCts = new(); + private readonly Dictionary> _lobbyWorldData = []; + private readonly Dictionary> _lobbyPoseData = []; + private readonly SemaphoreSlim _lobbyPoseDataModificationSemaphore = new(1, 1); + private readonly SemaphoreSlim _lobbyWorldDataModificationSemaphore = new(1, 1); + + public GPoseLobbyDistributionService(ILogger logger, IRedisDatabase redisDb, + IHubContext hubContext) + { + _logger = logger; + _redisDb = redisDb; + _hubContext = hubContext; + } + + private bool _disposed; + private readonly ILogger _logger; + private readonly IRedisDatabase _redisDb; + private readonly IHubContext _hubContext; + + public void Dispose() + { + if (_disposed) + { + return; + } + + _runtimeCts.Cancel(); + _runtimeCts.Dispose(); + _lobbyPoseDataModificationSemaphore.Dispose(); + _lobbyWorldDataModificationSemaphore.Dispose(); + + _disposed = true; + } + + public async Task PushWorldData(string lobby, string user, WorldData worldData) + { + await _lobbyWorldDataModificationSemaphore.WaitAsync().ConfigureAwait(false); + try + { + if (!_lobbyWorldData.TryGetValue(lobby, out var worldDataDict)) + { + _lobbyWorldData[lobby] = worldDataDict = new(StringComparer.Ordinal); + } + worldDataDict[user] = worldData; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Pushing World Data for Lobby {lobby} by User {user}", lobby, user); + } + finally + { + _lobbyWorldDataModificationSemaphore.Release(); + } + } + + public async Task PushPoseData(string lobby, string user, PoseData poseData) + { + await _lobbyPoseDataModificationSemaphore.WaitAsync().ConfigureAwait(false); + try + { + if (!_lobbyPoseData.TryGetValue(lobby, out var poseDataDict)) + { + _lobbyPoseData[lobby] = poseDataDict = new(StringComparer.Ordinal); + } + poseDataDict[user] = poseData; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Pushing World Data for Lobby {lobby} by User {user}", lobby, user); + } + finally + { + _lobbyPoseDataModificationSemaphore.Release(); + } + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _ = WorldDataDistribution(_runtimeCts.Token); + _ = PoseDataDistribution(_runtimeCts.Token); + + return Task.CompletedTask; + } + + private async Task WorldDataDistribution(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + try + { + await DistributeWorldData(token).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during World Data Distribution"); + } + await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false); + } + } + + private async Task PoseDataDistribution(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + try + { + await DistributePoseData(token).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Pose Data Distribution"); + } + await Task.Delay(TimeSpan.FromSeconds(2), token).ConfigureAwait(false); + } + } + + private async Task DistributeWorldData(CancellationToken token) + { + await _lobbyWorldDataModificationSemaphore.WaitAsync(token).ConfigureAwait(false); + Dictionary> clone = []; + try + { + clone = _lobbyWorldData.ToDictionary(k => k.Key, k => k.Value, StringComparer.Ordinal); + _lobbyWorldData.Clear(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Distributing World Data Clone generation"); + _lobbyWorldData.Clear(); + return; + } + finally + { + _lobbyWorldDataModificationSemaphore.Release(); + } + + foreach (var lobbyId in clone) + { + token.ThrowIfCancellationRequested(); + + try + { + if (!lobbyId.Value.Values.Any()) + continue; + + var gposeLobbyUsers = await _redisDb.GetAsync>($"GposeLobby:{lobbyId.Key}").ConfigureAwait(false); + if (gposeLobbyUsers == null) + continue; + + foreach (var data in lobbyId.Value) + { + await _hubContext.Clients.Users(gposeLobbyUsers.Where(k => !string.Equals(k, data.Key, StringComparison.Ordinal))) + .Client_GposeLobbyPushWorldData(new(data.Key), data.Value).ConfigureAwait(false); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during World Data Distribution for Lobby {lobby}", lobbyId.Key); + continue; + } + } + } + + private async Task DistributePoseData(CancellationToken token) + { + await _lobbyPoseDataModificationSemaphore.WaitAsync(token).ConfigureAwait(false); + Dictionary> clone = []; + try + { + clone = _lobbyPoseData.ToDictionary(k => k.Key, k => k.Value, StringComparer.Ordinal); + _lobbyPoseData.Clear(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Distributing Pose Data Clone generation"); + _lobbyPoseData.Clear(); + return; + } + finally + { + _lobbyPoseDataModificationSemaphore.Release(); + } + + foreach (var lobbyId in clone) + { + token.ThrowIfCancellationRequested(); + + try + { + if (!lobbyId.Value.Values.Any()) + continue; + + var gposeLobbyUsers = await _redisDb.GetAsync>($"GposeLobby:{lobbyId.Key}").ConfigureAwait(false); + if (gposeLobbyUsers == null) + continue; + + foreach (var data in lobbyId.Value) + { + await _hubContext.Clients.Users(gposeLobbyUsers.Where(k => !string.Equals(k, data.Key, StringComparison.Ordinal))) + .Client_GposeLobbyPushPoseData(new(data.Key), data.Value).ConfigureAwait(false); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Pose Data Distribution for Lobby {lobby}", lobbyId.Key); + continue; + } + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _runtimeCts.Cancel(); + return Task.CompletedTask; + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Services/SystemInfoService.cs b/MareSynchronosServer/MareSynchronosServer/Services/SystemInfoService.cs new file mode 100644 index 0000000..35db7ac --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Services/SystemInfoService.cs @@ -0,0 +1,98 @@ +using MareSynchronos.API.Dto; +using MareSynchronos.API.SignalR; +using MareSynchronosServer.Hubs; +using MareSynchronosShared.Data; +using MareSynchronosShared.Metrics; +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils.Configuration; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using StackExchange.Redis.Extensions.Core.Abstractions; + +namespace MareSynchronosServer.Services; + +public class SystemInfoService : IHostedService, IDisposable +{ + private readonly MareMetrics _mareMetrics; + private readonly IConfigurationService _config; + private readonly IServiceProvider _services; + private readonly ILogger _logger; + private readonly IHubContext _hubContext; + private readonly IRedisDatabase _redis; + private Timer _timer; + public SystemInfoDto SystemInfoDto { get; private set; } = new(); + + public SystemInfoService(MareMetrics mareMetrics, IConfigurationService configurationService, IServiceProvider services, + ILogger logger, IHubContext hubContext, IRedisDatabase redisDb) + { + _mareMetrics = mareMetrics; + _config = configurationService; + _services = services; + _logger = logger; + _hubContext = hubContext; + _redis = redisDb; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("System Info Service started"); + + var timeOut = _config.IsMain ? 5 : 15; + + _timer = new Timer(PushSystemInfo, null, TimeSpan.Zero, TimeSpan.FromSeconds(timeOut)); + + return Task.CompletedTask; + } + + private void PushSystemInfo(object state) + { + try + { + ThreadPool.GetAvailableThreads(out int workerThreads, out int ioThreads); + + _mareMetrics.SetGaugeTo(MetricsAPI.GaugeAvailableWorkerThreads, workerThreads); + _mareMetrics.SetGaugeTo(MetricsAPI.GaugeAvailableIOWorkerThreads, ioThreads); + + var onlineUsers = (_redis.SearchKeysAsync("UID:*").GetAwaiter().GetResult()).Count(); + SystemInfoDto = new SystemInfoDto() + { + OnlineUsers = onlineUsers, + }; + + if (_config.IsMain) + { + _logger.LogTrace("Sending System Info, Online Users: {onlineUsers}", onlineUsers); + + _hubContext.Clients.All.Client_UpdateSystemInfo(SystemInfoDto); + + using var scope = _services.CreateScope(); + using var db = scope.ServiceProvider.GetService()!; + + _mareMetrics.SetGaugeTo(MetricsAPI.GaugeAuthorizedConnections, onlineUsers); + _mareMetrics.SetGaugeTo(MetricsAPI.GaugePairs, db.ClientPairs.AsNoTracking().Count()); + _mareMetrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, db.ClientPairs.AsNoTracking().Count(p => p.IsPaused)); + _mareMetrics.SetGaugeTo(MetricsAPI.GaugeGroups, db.Groups.AsNoTracking().Count()); + _mareMetrics.SetGaugeTo(MetricsAPI.GaugeGroupPairs, db.GroupPairs.AsNoTracking().Count()); + _mareMetrics.SetGaugeTo(MetricsAPI.GaugeGroupPairsPaused, db.GroupPairs.AsNoTracking().Count(p => p.IsPaused)); + _mareMetrics.SetGaugeTo(MetricsAPI.GaugeUsersRegistered, db.Users.AsNoTracking().Count()); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to push system info"); + } + } + + + public Task StopAsync(CancellationToken cancellationToken) + { + _timer?.Change(Timeout.Infinite, 0); + + return Task.CompletedTask; + } + + public void Dispose() + { + _timer?.Dispose(); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServer/Services/UserCleanupService.cs b/MareSynchronosServer/MareSynchronosServer/Services/UserCleanupService.cs new file mode 100644 index 0000000..4d3bace --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Services/UserCleanupService.cs @@ -0,0 +1,195 @@ +using MareSynchronosShared.Data; +using MareSynchronosShared.Metrics; +using MareSynchronosShared.Models; +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils; +using MareSynchronosShared.Utils.Configuration; +using Microsoft.EntityFrameworkCore; + +namespace MareSynchronosServer.Services; + +public class UserCleanupService : IHostedService +{ + private readonly MareMetrics metrics; + private readonly ILogger _logger; + private readonly IDbContextFactory _mareDbContextFactory; + private readonly IConfigurationService _configuration; + private CancellationTokenSource _cleanupCts; + + public UserCleanupService(MareMetrics metrics, ILogger logger, IDbContextFactory mareDbContextFactory, IConfigurationService configuration) + { + this.metrics = metrics; + _logger = logger; + _mareDbContextFactory = mareDbContextFactory; + _configuration = configuration; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Cleanup Service started"); + _cleanupCts = new(); + + _ = CleanUp(_cleanupCts.Token); + + return Task.CompletedTask; + } + + private async Task CleanUp(CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + using (var dbContext = await _mareDbContextFactory.CreateDbContextAsync(ct).ConfigureAwait(false)) + { + + CleanUpOutdatedLodestoneAuths(dbContext); + + await PurgeUnusedAccounts(dbContext).ConfigureAwait(false); + + await PurgeTempInvites(dbContext).ConfigureAwait(false); + + dbContext.SaveChanges(); + } + + var now = DateTime.Now; + TimeOnly currentTime = new(now.Hour, now.Minute, now.Second); + TimeOnly futureTime = new(now.Hour, now.Minute - now.Minute % 10, 0); + var span = futureTime.AddMinutes(10) - currentTime; + + _logger.LogInformation("User Cleanup Complete, next run at {date}", now.Add(span)); + await Task.Delay(span, ct).ConfigureAwait(false); + } + } + + private async Task PurgeTempInvites(MareDbContext dbContext) + { + try + { + var tempInvites = await dbContext.GroupTempInvites.ToListAsync().ConfigureAwait(false); + dbContext.RemoveRange(tempInvites.Where(i => i.ExpirationDate < DateTime.UtcNow)); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error during Temp Invite purge"); + } + } + + private async Task PurgeUnusedAccounts(MareDbContext dbContext) + { + try + { + if (_configuration.GetValueOrDefault(nameof(ServerConfiguration.PurgeUnusedAccounts), false)) + { + var usersOlderThanDays = _configuration.GetValueOrDefault(nameof(ServerConfiguration.PurgeUnusedAccountsPeriodInDays), 14); + var maxGroupsByUser = _configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxGroupUserCount), 3); + + _logger.LogInformation("Cleaning up users older than {usersOlderThanDays} days", usersOlderThanDays); + + var allUsers = dbContext.Users.Where(u => string.IsNullOrEmpty(u.Alias)).ToList(); + List usersToRemove = new(); + foreach (var user in allUsers) + { + if (user.LastLoggedIn < DateTime.UtcNow - TimeSpan.FromDays(usersOlderThanDays)) + { + _logger.LogInformation("User outdated: {userUID}", user.UID); + usersToRemove.Add(user); + } + } + + foreach (var user in usersToRemove) + { + await SharedDbFunctions.PurgeUser(_logger, user, dbContext, maxGroupsByUser).ConfigureAwait(false); + } + } + + _logger.LogInformation("Cleaning up unauthorized users"); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error during user purge"); + } + } + + private void CleanUpOutdatedLodestoneAuths(MareDbContext dbContext) + { + try + { + _logger.LogInformation($"Cleaning up expired lodestone authentications"); + var lodestoneAuths = dbContext.LodeStoneAuth.Include(u => u.User).Where(a => a.StartedAt != null).ToList(); + List expiredAuths = new List(); + foreach (var auth in lodestoneAuths) + { + if (auth.StartedAt < DateTime.UtcNow - TimeSpan.FromMinutes(15)) + { + expiredAuths.Add(auth); + } + } + + dbContext.Users.RemoveRange(expiredAuths.Where(u => u.User != null).Select(a => a.User)); + dbContext.RemoveRange(expiredAuths); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error during expired auths cleanup"); + } + } + + public async Task PurgeUser(User user, MareDbContext dbContext) + { + _logger.LogInformation("Purging user: {uid}", user.UID); + + var lodestone = dbContext.LodeStoneAuth.SingleOrDefault(a => a.User.UID == user.UID); + + if (lodestone != null) + { + dbContext.Remove(lodestone); + } + + var auth = dbContext.Auth.Single(a => a.UserUID == user.UID); + + var ownPairData = dbContext.ClientPairs.Where(u => u.User.UID == user.UID).ToList(); + dbContext.ClientPairs.RemoveRange(ownPairData); + var otherPairData = dbContext.ClientPairs.Include(u => u.User) + .Where(u => u.OtherUser.UID == user.UID).ToList(); + dbContext.ClientPairs.RemoveRange(otherPairData); + + var userJoinedGroups = await dbContext.GroupPairs.Include(g => g.Group).Where(u => u.GroupUserUID == user.UID).ToListAsync().ConfigureAwait(false); + + foreach (var userGroupPair in userJoinedGroups) + { + bool ownerHasLeft = string.Equals(userGroupPair.Group.OwnerUID, user.UID, StringComparison.Ordinal); + + if (ownerHasLeft) + { + var groupPairs = await dbContext.GroupPairs.Where(g => g.GroupGID == userGroupPair.GroupGID && g.GroupUserUID != user.UID).ToListAsync().ConfigureAwait(false); + + if (!groupPairs.Any()) + { + _logger.LogInformation("Group {gid} has no new owner, deleting", userGroupPair.GroupGID); + dbContext.Groups.Remove(userGroupPair.Group); + } + else + { + _ = await SharedDbFunctions.MigrateOrDeleteGroup(dbContext, userGroupPair.Group, groupPairs, _configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxExistingGroupsByUser), 3)).ConfigureAwait(false); + } + } + + dbContext.GroupPairs.Remove(userGroupPair); + + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + + _logger.LogInformation("User purged: {uid}", user.UID); + + dbContext.Auth.Remove(auth); + dbContext.Users.Remove(user); + + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _cleanupCts.Cancel(); + + return Task.CompletedTask; + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Startup.cs b/MareSynchronosServer/MareSynchronosServer/Startup.cs new file mode 100644 index 0000000..da4ec70 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Startup.cs @@ -0,0 +1,343 @@ +using Microsoft.EntityFrameworkCore; +using MareSynchronosServer.Hubs; +using Microsoft.AspNetCore.Http.Connections; +using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.Authorization; +using AspNetCoreRateLimit; +using MareSynchronosShared.Data; +using MareSynchronosShared.Metrics; +using MareSynchronosServer.Services; +using MareSynchronosShared.Utils; +using MareSynchronosShared.Services; +using Prometheus; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using System.Text; +using StackExchange.Redis; +using StackExchange.Redis.Extensions.Core.Configuration; +using System.Net; +using StackExchange.Redis.Extensions.System.Text.Json; +using MareSynchronos.API.SignalR; +using MessagePack; +using MessagePack.Resolvers; +using Microsoft.AspNetCore.Mvc.Controllers; +using MareSynchronosServer.Controllers; +using MareSynchronosShared.RequirementHandlers; +using MareSynchronosShared.Utils.Configuration; + +namespace MareSynchronosServer; + +public class Startup +{ + private readonly ILogger _logger; + + public Startup(IConfiguration configuration, ILogger logger) + { + Configuration = configuration; + _logger = logger; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddHttpContextAccessor(); + + services.AddTransient(_ => Configuration); + + var mareConfig = Configuration.GetRequiredSection("MareSynchronos"); + + // configure metrics + ConfigureMetrics(services); + + // configure database + ConfigureDatabase(services, mareConfig); + + // configure authentication and authorization + ConfigureAuthorization(services); + + // configure rate limiting + ConfigureIpRateLimiting(services); + + // configure SignalR + ConfigureSignalR(services, mareConfig); + + // configure mare specific services + ConfigureMareServices(services, mareConfig); + + services.AddHealthChecks(); + services.AddControllers().ConfigureApplicationPartManager(a => + { + a.FeatureProviders.Remove(a.FeatureProviders.OfType().First()); + if (mareConfig.GetValue(nameof(ServerConfiguration.MainServerAddress), defaultValue: null) == null) + { + a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(MareServerConfigurationController), typeof(MareBaseConfigurationController), typeof(ClientMessageController), typeof(MainController))); + } + else + { + a.FeatureProviders.Add(new AllowedControllersFeatureProvider()); + } + }); + } + + private void ConfigureMareServices(IServiceCollection services, IConfigurationSection mareConfig) + { + bool isMainServer = mareConfig.GetValue(nameof(ServerConfiguration.MainServerAddress), defaultValue: null) == null; + + services.Configure(Configuration.GetRequiredSection("MareSynchronos")); + services.Configure(Configuration.GetRequiredSection("MareSynchronos")); + + services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(provider => provider.GetService()); + // configure services based on main server status + ConfigureServicesBasedOnShardType(services, mareConfig, isMainServer); + + if (isMainServer) + { + services.AddSingleton(); + services.AddHostedService(provider => provider.GetService()); + services.AddSingleton(); + services.AddHostedService(provider => provider.GetService()); + } + + services.AddSingleton(); + services.AddHostedService(provider => provider.GetService()); + } + + private static void ConfigureSignalR(IServiceCollection services, IConfigurationSection mareConfig) + { + services.AddSingleton(); + + var signalRServiceBuilder = services.AddSignalR(hubOptions => + { + hubOptions.MaximumReceiveMessageSize = long.MaxValue; + hubOptions.EnableDetailedErrors = true; + hubOptions.MaximumParallelInvocationsPerClient = 10; + hubOptions.StreamBufferCapacity = 200; + + hubOptions.AddFilter(); + }).AddMessagePackProtocol(opt => + { + var resolver = CompositeResolver.Create(StandardResolverAllowPrivate.Instance, + BuiltinResolver.Instance, + AttributeFormatterResolver.Instance, + // replace enum resolver + DynamicEnumAsStringResolver.Instance, + DynamicGenericResolver.Instance, + DynamicUnionResolver.Instance, + DynamicObjectResolver.Instance, + PrimitiveObjectResolver.Instance, + // final fallback(last priority) + StandardResolver.Instance); + + opt.SerializerOptions = MessagePackSerializerOptions.Standard + .WithCompression(MessagePackCompression.Lz4Block) + .WithResolver(resolver); + }); + + // configure redis for SignalR + var redisConnection = mareConfig.GetValue(nameof(ServerConfiguration.RedisConnectionString), string.Empty); + signalRServiceBuilder.AddStackExchangeRedis(redisConnection, options => { }); + + var options = ConfigurationOptions.Parse(redisConnection); + + var endpoint = options.EndPoints[0]; + string address = ""; + int port = 0; + if (endpoint is DnsEndPoint dnsEndPoint) { address = dnsEndPoint.Host; port = dnsEndPoint.Port; } + if (endpoint is IPEndPoint ipEndPoint) { address = ipEndPoint.Address.ToString(); port = ipEndPoint.Port; } + var redisConfiguration = new RedisConfiguration() + { + AbortOnConnectFail = true, + KeyPrefix = "", + Hosts = new RedisHost[] + { + new RedisHost(){ Host = address, Port = port }, + }, + AllowAdmin = true, + ConnectTimeout = options.ConnectTimeout, + Database = 0, + Ssl = false, + Password = options.Password, + ServerEnumerationStrategy = new ServerEnumerationStrategy() + { + Mode = ServerEnumerationStrategy.ModeOptions.All, + TargetRole = ServerEnumerationStrategy.TargetRoleOptions.Any, + UnreachableServerAction = ServerEnumerationStrategy.UnreachableServerActionOptions.Throw, + }, + MaxValueLength = 1024, + PoolSize = mareConfig.GetValue(nameof(ServerConfiguration.RedisPool), 50), + SyncTimeout = options.SyncTimeout, + }; + + services.AddStackExchangeRedisExtensions(redisConfiguration); + } + + private void ConfigureIpRateLimiting(IServiceCollection services) + { + services.Configure(Configuration.GetSection("IpRateLimiting")); + services.Configure(Configuration.GetSection("IpRateLimitPolicies")); + services.AddSingleton(); + services.AddMemoryCache(); + services.AddInMemoryRateLimiting(); + } + + private static void ConfigureAuthorization(IServiceCollection services) + { + services.AddTransient(); + + services.AddOptions(JwtBearerDefaults.AuthenticationScheme) + .Configure>((options, config) => + { + options.TokenValidationParameters = new() + { + ValidateIssuer = false, + ValidateLifetime = false, + ValidateAudience = false, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.GetValue(nameof(MareConfigurationBase.Jwt)))), + }; + }); + + services.AddAuthentication(o => + { + o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(); + + services.AddAuthorization(options => + { + options.DefaultPolicy = new AuthorizationPolicyBuilder() + .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme) + .RequireAuthenticatedUser().Build(); + options.AddPolicy("Authenticated", policy => + { + policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme); + policy.RequireAuthenticatedUser(); + }); + options.AddPolicy("Identified", policy => + { + policy.AddRequirements(new UserRequirement(UserRequirements.Identified)); + }); + options.AddPolicy("Admin", policy => + { + policy.AddRequirements(new UserRequirement(UserRequirements.Identified | UserRequirements.Administrator)); + }); + options.AddPolicy("Moderator", policy => + { + policy.AddRequirements(new UserRequirement(UserRequirements.Identified | UserRequirements.Moderator | UserRequirements.Administrator)); + }); + options.AddPolicy("Internal", new AuthorizationPolicyBuilder().RequireClaim(MareClaimTypes.Internal, "true").Build()); + }); + } + + private void ConfigureDatabase(IServiceCollection services, IConfigurationSection mareConfig) + { + services.AddDbContextPool(options => + { + options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder => + { + builder.MigrationsHistoryTable("_efmigrationshistory", "public"); + builder.MigrationsAssembly("MareSynchronosShared"); + }).UseSnakeCaseNamingConvention(); + options.EnableThreadSafetyChecks(false); + }, mareConfig.GetValue(nameof(MareConfigurationBase.DbContextPoolSize), 1024)); + services.AddDbContextFactory(options => + { + options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder => + { + builder.MigrationsHistoryTable("_efmigrationshistory", "public"); + builder.MigrationsAssembly("MareSynchronosShared"); + }).UseSnakeCaseNamingConvention(); + options.EnableThreadSafetyChecks(false); + }); + } + + private static void ConfigureMetrics(IServiceCollection services) + { + services.AddSingleton(m => new MareMetrics(m.GetService>(), new List + { + MetricsAPI.CounterInitializedConnections, + MetricsAPI.CounterUserPushData, + MetricsAPI.CounterUserPushDataTo, + MetricsAPI.CounterUsersRegisteredDeleted, + MetricsAPI.CounterAuthenticationCacheHits, + MetricsAPI.CounterAuthenticationFailures, + MetricsAPI.CounterAuthenticationRequests, + MetricsAPI.CounterAuthenticationSuccesses, + }, new List + { + MetricsAPI.GaugeAuthorizedConnections, + MetricsAPI.GaugeConnections, + MetricsAPI.GaugePairs, + MetricsAPI.GaugePairsPaused, + MetricsAPI.GaugeAvailableIOWorkerThreads, + MetricsAPI.GaugeAvailableWorkerThreads, + MetricsAPI.GaugeGroups, + MetricsAPI.GaugeGroupPairs, + MetricsAPI.GaugeGroupPairsPaused, + MetricsAPI.GaugeUsersRegistered, + MetricsAPI.GaugeGposeLobbies, + MetricsAPI.GaugeGposeLobbyUsers + })); + } + + private static void ConfigureServicesBasedOnShardType(IServiceCollection services, IConfigurationSection mareConfig, bool isMainServer) + { + if (!isMainServer) + { + services.AddSingleton, MareConfigurationServiceClient>(); + services.AddSingleton, MareConfigurationServiceClient>(); + + services.AddHostedService(p => (MareConfigurationServiceClient)p.GetService>()); + services.AddHostedService(p => (MareConfigurationServiceClient)p.GetService>()); + } + else + { + services.AddSingleton, MareConfigurationServiceServer>(); + services.AddSingleton, MareConfigurationServiceServer>(); + } + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger logger) + { + logger.LogInformation("Running Configure"); + + var config = app.ApplicationServices.GetRequiredService>(); + + app.UseIpRateLimiting(); + + app.UseRouting(); + + app.UseWebSockets(); + app.UseHttpMetrics(); + + var metricServer = new KestrelMetricServer(config.GetValueOrDefault(nameof(MareConfigurationBase.MetricsPort), 4980)); + metricServer.Start(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapHub(IMareHub.Path, options => + { + options.ApplicationMaxBufferSize = 5242880; + options.TransportMaxBufferSize = 5242880; + options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling; + }); + + endpoints.MapHealthChecks("/health").AllowAnonymous(); + endpoints.MapControllers(); + + foreach (var source in endpoints.DataSources.SelectMany(e => e.Endpoints).Cast()) + { + if (source == null) continue; + _logger.LogInformation("Endpoint: {url} ", source.RoutePattern.RawText); + } + }); + + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Utils/Extensions.cs b/MareSynchronosServer/MareSynchronosServer/Utils/Extensions.cs new file mode 100644 index 0000000..a9644ff --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Utils/Extensions.cs @@ -0,0 +1,53 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Data.Extensions; +using MareSynchronosShared.Models; + +namespace MareSynchronosServer.Utils +{ + public static class Extensions + { + public static GroupData ToGroupData(this Group group) + { + return new GroupData(group.GID, group.Alias); + } + + public static UserData ToUserData(this GroupPair pair) + { + return new UserData(pair.GroupUser.UID, pair.GroupUser.Alias); + } + + public static UserData ToUserData(this User user) + { + return new UserData(user.UID, user.Alias); + } + + public static GroupPermissions GetGroupPermissions(this Group group) + { + var permissions = GroupPermissions.NoneSet; + permissions.SetDisableAnimations(group.DisableAnimations); + permissions.SetDisableSounds(group.DisableSounds); + permissions.SetDisableInvites(!group.InvitesEnabled); + permissions.SetDisableVFX(group.DisableVFX); + return permissions; + } + + public static GroupUserPermissions GetGroupPairPermissions(this GroupPair groupPair) + { + var permissions = GroupUserPermissions.NoneSet; + permissions.SetDisableAnimations(groupPair.DisableAnimations); + permissions.SetDisableSounds(groupPair.DisableSounds); + permissions.SetPaused(groupPair.IsPaused); + permissions.SetDisableVFX(groupPair.DisableVFX); + return permissions; + } + + public static GroupUserInfo GetGroupPairUserInfo(this GroupPair groupPair) + { + var groupUserInfo = GroupUserInfo.None; + groupUserInfo.SetPinned(groupPair.IsPinned); + groupUserInfo.SetModerator(groupPair.IsModerator); + return groupUserInfo; + } + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Utils/MareHubLogger.cs b/MareSynchronosServer/MareSynchronosServer/Utils/MareHubLogger.cs new file mode 100644 index 0000000..91a95e2 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Utils/MareHubLogger.cs @@ -0,0 +1,33 @@ +using MareSynchronosServer.Hubs; +using System.Runtime.CompilerServices; + +namespace MareSynchronosServer.Utils; + +public class MareHubLogger +{ + private readonly MareHub _hub; + private readonly ILogger _logger; + + public MareHubLogger(MareHub hub, ILogger logger) + { + _hub = hub; + _logger = logger; + } + + public static object[] Args(params object[] args) + { + return args; + } + + public void LogCallInfo(object[] args = null, [CallerMemberName] string methodName = "") + { + string formattedArgs = args != null && args.Length != 0 ? "|" + string.Join(":", args) : string.Empty; + _logger.LogInformation("{uid}:{method}{args}", _hub.UserUID, methodName, formattedArgs); + } + + public void LogCallWarning(object[] args = null, [CallerMemberName] string methodName = "") + { + string formattedArgs = args != null && args.Length != 0 ? "|" + string.Join(":", args) : string.Empty; + _logger.LogWarning("{uid}:{method}{args}", _hub.UserUID, methodName, formattedArgs); + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Utils/PauseInfo.cs b/MareSynchronosServer/MareSynchronosServer/Utils/PauseInfo.cs new file mode 100644 index 0000000..987b195 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Utils/PauseInfo.cs @@ -0,0 +1,8 @@ +namespace MareSynchronosServer.Utils; + +public enum PauseInfo +{ + NoConnection, + Paused, + Unpaused, +} diff --git a/MareSynchronosServer/MareSynchronosServer/Utils/PauseState.cs b/MareSynchronosServer/MareSynchronosServer/Utils/PauseState.cs new file mode 100644 index 0000000..f656c3a --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Utils/PauseState.cs @@ -0,0 +1,9 @@ +namespace MareSynchronosServer.Utils; + +public record PauseState +{ + public string GID { get; set; } + public bool IsPaused => IsSelfPaused || IsOtherPaused; + public bool IsSelfPaused { get; set; } + public bool IsOtherPaused { get; set; } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServer/Utils/PausedEntry.cs b/MareSynchronosServer/MareSynchronosServer/Utils/PausedEntry.cs new file mode 100644 index 0000000..13de566 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Utils/PausedEntry.cs @@ -0,0 +1,58 @@ +namespace MareSynchronosServer.Utils; + +public record PausedEntry +{ + public string UID { get; set; } + public List PauseStates { get; set; } = new(); + + public PauseInfo IsDirectlyPaused => PauseStateWithoutGroups == null ? PauseInfo.NoConnection + : PauseStates.First(g => g.GID == null).IsPaused ? PauseInfo.Paused : PauseInfo.Unpaused; + + public PauseInfo IsPausedPerGroup => !PauseStatesWithoutDirect.Any() ? PauseInfo.NoConnection + : PauseStatesWithoutDirect.All(p => p.IsPaused) ? PauseInfo.Paused : PauseInfo.Unpaused; + + private IEnumerable PauseStatesWithoutDirect => PauseStates.Where(f => f.GID != null); + private PauseState PauseStateWithoutGroups => PauseStates.SingleOrDefault(p => p.GID == null); + + public bool IsPaused + { + get + { + var isDirectlyPaused = IsDirectlyPaused; + bool result; + if (isDirectlyPaused != PauseInfo.NoConnection) + { + result = isDirectlyPaused == PauseInfo.Paused; + } + else + { + result = IsPausedPerGroup == PauseInfo.Paused; + } + + return result; + } + } + + public PauseInfo IsOtherPausedForSpecificGroup(string gid) + { + var state = PauseStatesWithoutDirect.SingleOrDefault(g => string.Equals(g.GID, gid, StringComparison.Ordinal)); + if (state == null) return PauseInfo.NoConnection; + return state.IsOtherPaused ? PauseInfo.Paused : PauseInfo.Unpaused; + } + + public PauseInfo IsPausedForSpecificGroup(string gid) + { + var state = PauseStatesWithoutDirect.SingleOrDefault(g => string.Equals(g.GID, gid, StringComparison.Ordinal)); + if (state == null) return PauseInfo.NoConnection; + return state.IsPaused ? PauseInfo.Paused : PauseInfo.NoConnection; + } + + public PauseInfo IsPausedExcludingGroup(string gid) + { + var states = PauseStatesWithoutDirect.Where(f => !string.Equals(f.GID, gid, StringComparison.Ordinal)).ToList(); + if (!states.Any()) return PauseInfo.NoConnection; + var result = states.All(p => p.IsPaused); + if (result) return PauseInfo.Paused; + return PauseInfo.Unpaused; + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Utils/UserPair.cs b/MareSynchronosServer/MareSynchronosServer/Utils/UserPair.cs new file mode 100644 index 0000000..1a5c367 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Utils/UserPair.cs @@ -0,0 +1,12 @@ +namespace MareSynchronosServer.Hubs; + +public partial class MareHub +{ + private record UserPair + { + public string UserUID { get; set; } + public string OtherUserUID { get; set; } + public bool UserPausedOther { get; set; } + public bool OtherPausedUser { get; set; } + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/appsettings.Development.json b/MareSynchronosServer/MareSynchronosServer/appsettings.Development.json new file mode 100644 index 0000000..5173757 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/appsettings.json b/MareSynchronosServer/MareSynchronosServer/appsettings.json new file mode 100644 index 0000000..38ca83d --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/appsettings.json @@ -0,0 +1,61 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5432;Database=mare;Username=postgres" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "MareSynchronosServer.Authentication": "Warning", + "System.IO.IOException": "Warning" + }, + "File": { + "BasePath": "logs", + "FileAccessMode": "KeepOpenAndAutoFlush", + "FileEncodingName": "utf-8", + "DateFormat": "yyyMMdd", + "MaxFileSize": 10485760, + "Files": [ + { + "Path": "mare-.log" + } + ] + } + }, + "MareSynchronos": { + "DbContextPoolSize": 2000, + "CdnFullUrl": "https:///cache/", + "ServiceAddress": "http://localhost:5002", + "StaticFileServiceAddress": "http://localhost:5003" + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "Https": { + "Url": "https://+:5000", + "Certificate": { + "Subject": "darkarchon.internet-box.ch", + "Store": "My", + "Location": "LocalMachine" + //"AllowInvalid": false + // "Path": "", //use path, keypath and password to provide a valid certificate if not using windows key store + // "KeyPath": "" + // "Password": "" + } + } + } + }, + "IpRateLimiting": { + "EnableEndpointRateLimiting": false, + "StackBlockedRequests": false, + "RealIpHeader": "X-Real-IP", + "ClientIdHeader": "X-ClientId", + "HttpStatusCode": 429, + "IpWhitelist": [ ], + "GeneralRules": [ ] + }, + "IPRateLimitPolicies": { + "IpRules": [] + } +} diff --git a/MareSynchronosServer/MareSynchronosServerTest/Discord/DiscordBotTest.cs b/MareSynchronosServer/MareSynchronosServerTest/Discord/DiscordBotTest.cs new file mode 100644 index 0000000..6a6cb47 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServerTest/Discord/DiscordBotTest.cs @@ -0,0 +1,53 @@ +using FluentAssertions; +using MareSynchronosServer.Discord; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace MareSynchronosServerTest.Discord { + public class DiscordBotTest { + + [Test] + [TestCase("", null)] + [TestCase("abcd", null)] + [TestCase("www.google.de", null)] + [TestCase("https://www.google.de", null)] + [TestCase("de.finalfantasyxiv.com/lodestone/character/1234", null)] + [TestCase("https://cn.finalfantasyxiv.com/lodestone/character/1234", null)] + [TestCase("http://jp.finalfantasyxiv.com/lodestone/character/1234", null)] + [TestCase("https://jp.finalfantasyxiv.com/character/1234", null)] + [TestCase("https://jp.finalfantasyxiv.com/lodestone/1234", null)] + [TestCase("https://www.finalfantasyxiv.com/lodestone/character/1234", null)] + [TestCase("https://jp.finalfantasyxiv.com/lodestone/character/1234", 1234)] + [TestCase("https://fr.finalfantasyxiv.com/lodestone/character/1234", 1234)] + [TestCase("https://eu.finalfantasyxiv.com/lodestone/character/1234/", 1234)] + [TestCase("https://eu.finalfantasyxiv.com/lodestone/character/1234?myurlparameter=500", 1234)] + [TestCase("https://de.finalfantasyxiv.com/lodestone/character/1234/whatever/3456", 1234)] + [TestCase("https://na.finalfantasyxiv.com/lodestone/character/1234abcd4321/whatever/3456", 1234)] + public void ParseCharacterIdFromLodestoneUrl_CheckThatIdIsParsedCorrectly(string url, int? expectedId) { + var inMemorySettings = new Dictionary { + {"DiscordBotToken", "1234"} + }; + + IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(inMemorySettings) + .Build(); + + var spMock = new Mock(); + var loggerMock = new Mock>(); + + var sut = new DiscordBot(spMock.Object, configuration, loggerMock.Object); + MethodInfo methodInfo = sut.GetType().GetMethod("ParseCharacterIdFromLodestoneUrl", BindingFlags.NonPublic | BindingFlags.Instance); + var actualId = methodInfo.Invoke(sut, new object[] { url }); + + actualId.Should().Be(expectedId); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosServerTest/Hubs/MareHubTest.cs b/MareSynchronosServer/MareSynchronosServerTest/Hubs/MareHubTest.cs new file mode 100644 index 0000000..4f8394e --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServerTest/Hubs/MareHubTest.cs @@ -0,0 +1,82 @@ +using MareSynchronosServer.Hubs; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using MareSynchronosShared.Data; +using MareSynchronosShared.Models; + +namespace MareSynchronosServerTest.Hubs { + public class MareHubTest { + + [Test] + public async Task Disconnect_QueryReturnsCorrectResult_Test() { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: "mare").Options; + + using var context = new MareDbContext(options); + context.Users.Add(new User() { UID = "User1", IsModerator = false, IsAdmin = false, CharacterIdentification = "Ident1" }); + context.Users.Add(new User() { UID = "User2", IsModerator = false, IsAdmin = false, CharacterIdentification = "Ident2" }); + context.Users.Add(new User() { UID = "User3", IsModerator = false, IsAdmin = false, CharacterIdentification = "Ident3" }); + context.Users.Add(new User() { UID = "User4", IsModerator = false, IsAdmin = false, CharacterIdentification = "Ident4" }); + context.Users.Add(new User() { UID = "User5", IsModerator = false, IsAdmin = false, CharacterIdentification = "Ident5" }); + context.Users.Add(new User() { UID = "User6", IsModerator = false, IsAdmin = false, CharacterIdentification = "Ident6" }); + + // Valid pairs + context.ClientPairs.Add(new ClientPair() { UserUID = "User1", OtherUserUID = "User2", IsPaused = false }); + context.ClientPairs.Add(new ClientPair() { UserUID = "User2", OtherUserUID = "User1", IsPaused = false }); + context.ClientPairs.Add(new ClientPair() { UserUID = "User1", OtherUserUID = "User3", IsPaused = false }); + context.ClientPairs.Add(new ClientPair() { UserUID = "User3", OtherUserUID = "User1", IsPaused = false }); + + // Other user paired but user not paired with current user + context.ClientPairs.Add(new ClientPair() { UserUID = "User4", OtherUserUID = "User1", IsPaused = false }); + + // Valid pair but user paused + context.ClientPairs.Add(new ClientPair() { UserUID = "User1", OtherUserUID = "User5", IsPaused = true }); + context.ClientPairs.Add(new ClientPair() { UserUID = "User5", OtherUserUID = "User1", IsPaused = false }); + + // Valid pair but other user paused + context.ClientPairs.Add(new ClientPair() { UserUID = "User1", OtherUserUID = "User6", IsPaused = false }); + context.ClientPairs.Add(new ClientPair() { UserUID = "User6", OtherUserUID = "User1", IsPaused = true }); + + // Non existant user + context.ClientPairs.Add(new ClientPair() { UserUID = "User99", OtherUserUID = "User1", IsPaused = false }); + + // Non-related data + context.ClientPairs.Add(new ClientPair() { UserUID = "User6", OtherUserUID = "User4", IsPaused = true }); + context.ClientPairs.Add(new ClientPair() { UserUID = "User4", OtherUserUID = "User3", IsPaused = false }); + context.ClientPairs.Add(new ClientPair() { UserUID = "User3", OtherUserUID = "User2", IsPaused = false }); + + context.SaveChanges(); + + var clientContextMock = new Mock(); + var claimMock = new Mock(); + var claim = new Claim(ClaimTypes.NameIdentifier, "User1"); + claimMock.SetupGet(x => x.Claims).Returns(new List() { claim }); + clientContextMock.SetupGet(x => x.User).Returns(claimMock.Object); + + var clientsMock = new Mock(); + var clientProxyMock = new Mock(); + clientsMock.Setup(x => x.Users(It.IsAny>())).Returns(clientProxyMock.Object); + + var hub = new MareHub(context, new Mock>().Object, null, new Mock().Object, new Mock().Object); + + + hub.Clients = clientsMock.Object; + hub.Context = clientContextMock.Object; + + await hub.OnDisconnectedAsync(new Exception("Test Exception")).ConfigureAwait(false); + + clientsMock.Verify(x => x.Users(It.Is>(x => x.Count() == 2 && x[0] == "User2" && x[1] == "User3")), Times.Once); + clientProxyMock.Verify(x => x.SendCoreAsync(It.IsAny(), It.Is(o => (string)o[0] == "Ident1"), It.IsAny()), Times.Once); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosServerTest/MareSynchronosServerTest.csproj b/MareSynchronosServer/MareSynchronosServerTest/MareSynchronosServerTest.csproj new file mode 100644 index 0000000..732f04e --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServerTest/MareSynchronosServerTest.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + + + + + diff --git a/MareSynchronosServer/MareSynchronosServerTest/Usings.cs b/MareSynchronosServer/MareSynchronosServerTest/Usings.cs new file mode 100644 index 0000000..cefced4 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServerTest/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServices/.config/dotnet-tools.json b/MareSynchronosServer/MareSynchronosServices/.config/dotnet-tools.json new file mode 100644 index 0000000..98091c9 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "6.0.8", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBot.cs b/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBot.cs new file mode 100644 index 0000000..bf17215 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBot.cs @@ -0,0 +1,421 @@ +using Discord; +using Discord.Interactions; +using Discord.Rest; +using Discord.WebSocket; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Dto.User; +using MareSynchronos.API.SignalR; +using MareSynchronosServer.Hubs; +using MareSynchronosShared.Data; +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils.Configuration; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using StackExchange.Redis; +using System.Text; + +namespace MareSynchronosServices.Discord; + +internal class DiscordBot : IHostedService +{ + private readonly DiscordBotServices _botServices; + private readonly IConfigurationService _configurationService; + private readonly IConnectionMultiplexer _connectionMultiplexer; + private readonly DiscordSocketClient _discordClient; + private readonly ILogger _logger; + private readonly IHubContext _mareHubContext; + private readonly IServiceProvider _services; + private InteractionService _interactionModule; + private CancellationTokenSource? _processReportQueueCts; + private CancellationTokenSource? _updateStatusCts; + private CancellationTokenSource? _vanityUpdateCts; + + public DiscordBot(DiscordBotServices botServices, IServiceProvider services, IConfigurationService configuration, + IHubContext mareHubContext, + ILogger logger, IConnectionMultiplexer connectionMultiplexer) + { + _botServices = botServices; + _services = services; + _configurationService = configuration; + _mareHubContext = mareHubContext; + _logger = logger; + _connectionMultiplexer = connectionMultiplexer; + _discordClient = new(new DiscordSocketConfig() + { + DefaultRetryMode = RetryMode.AlwaysRetry + }); + + _discordClient.Log += Log; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + var token = _configurationService.GetValueOrDefault(nameof(ServicesConfiguration.DiscordBotToken), string.Empty); + if (!string.IsNullOrEmpty(token)) + { + _interactionModule = new InteractionService(_discordClient); + await _interactionModule.AddModuleAsync(typeof(MareModule), _services).ConfigureAwait(false); + + await _discordClient.LoginAsync(TokenType.Bot, token).ConfigureAwait(false); + await _discordClient.StartAsync().ConfigureAwait(false); + + _discordClient.Ready += DiscordClient_Ready; + _discordClient.ButtonExecuted += ButtonExecutedHandler; + _discordClient.InteractionCreated += async (x) => + { + var ctx = new SocketInteractionContext(_discordClient, x); + await _interactionModule.ExecuteCommandAsync(ctx, _services); + }; + + await _botServices.Start(); + _ = UpdateStatusAsync(); + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + if (!string.IsNullOrEmpty(_configurationService.GetValueOrDefault(nameof(ServicesConfiguration.DiscordBotToken), string.Empty))) + { + _discordClient.ButtonExecuted -= ButtonExecutedHandler; + + await _botServices.Stop(); + _processReportQueueCts?.Cancel(); + _updateStatusCts?.Cancel(); + _vanityUpdateCts?.Cancel(); + + await _discordClient.LogoutAsync().ConfigureAwait(false); + await _discordClient.StopAsync().ConfigureAwait(false); + } + } + + private async Task ButtonExecutedHandler(SocketMessageComponent arg) + { + var id = arg.Data.CustomId; + if (!id.StartsWith("mare-report-button", StringComparison.Ordinal)) return; + + var userId = arg.User.Id; + using var scope = _services.CreateScope(); + using var dbContext = scope.ServiceProvider.GetRequiredService(); + var user = await dbContext.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.DiscordId == userId).ConfigureAwait(false); + + if (user == null || (!user.User.IsModerator && !user.User.IsAdmin)) + { + EmbedBuilder eb = new(); + eb.WithTitle($"Cannot resolve report"); + eb.WithDescription($"<@{userId}>: You have no rights to resolve this report"); + await arg.RespondAsync(embed: eb.Build()).ConfigureAwait(false); + return; + } + + id = id.Remove(0, "mare-report-button-".Length); + var split = id.Split('-', StringSplitOptions.RemoveEmptyEntries); + + var profile = await dbContext.UserProfileData.SingleAsync(u => u.UserUID == split[1]).ConfigureAwait(false); + + var embed = arg.Message.Embeds.First(); + + var builder = embed.ToEmbedBuilder(); + var otherPairs = await dbContext.ClientPairs.Where(p => p.UserUID == split[1]).Select(p => p.OtherUserUID).ToListAsync().ConfigureAwait(false); + switch (split[0]) + { + case "dismiss": + builder.AddField("Resolution", $"Dismissed by <@{userId}>"); + builder.WithColor(Color.Green); + profile.FlaggedForReport = false; + break; + + case "banreporting": + builder.AddField("Resolution", $"Dismissed by <@{userId}>, Reporting user banned"); + builder.WithColor(Color.DarkGreen); + profile.FlaggedForReport = false; + var reportingUser = await dbContext.Auth.SingleAsync(u => u.UserUID == split[2]).ConfigureAwait(false); + reportingUser.IsBanned = true; + var regReporting = await dbContext.LodeStoneAuth.SingleAsync(u => u.User.UID == reportingUser.UserUID).ConfigureAwait(false); + dbContext.BannedRegistrations.Add(new MareSynchronosShared.Models.BannedRegistrations() + { + DiscordIdOrLodestoneAuth = regReporting.HashedLodestoneId + }); + dbContext.BannedRegistrations.Add(new MareSynchronosShared.Models.BannedRegistrations() + { + DiscordIdOrLodestoneAuth = regReporting.DiscordId.ToString() + }); + break; + + case "banprofile": + builder.AddField("Resolution", $"Profile has been banned by <@{userId}>"); + builder.WithColor(Color.Red); + profile.Base64ProfileImage = null; + profile.UserDescription = null; + profile.ProfileDisabled = true; + profile.FlaggedForReport = false; + break; + + case "banuser": + builder.AddField("Resolution", $"User has been banned by <@{userId}>"); + builder.WithColor(Color.DarkRed); + var offendingUser = await dbContext.Auth.SingleAsync(u => u.UserUID == split[1]).ConfigureAwait(false); + offendingUser.IsBanned = true; + profile.Base64ProfileImage = null; + profile.UserDescription = null; + profile.ProfileDisabled = true; + var reg = await dbContext.LodeStoneAuth.SingleAsync(u => u.User.UID == offendingUser.UserUID).ConfigureAwait(false); + dbContext.BannedRegistrations.Add(new MareSynchronosShared.Models.BannedRegistrations() + { + DiscordIdOrLodestoneAuth = reg.HashedLodestoneId + }); + dbContext.BannedRegistrations.Add(new MareSynchronosShared.Models.BannedRegistrations() + { + DiscordIdOrLodestoneAuth = reg.DiscordId.ToString() + }); + break; + } + + await dbContext.SaveChangesAsync().ConfigureAwait(false); + + await _mareHubContext.Clients.Users(otherPairs).SendAsync(nameof(IMareHub.Client_UserUpdateProfile), new UserDto(new(split[1]))).ConfigureAwait(false); + await _mareHubContext.Clients.User(split[1]).SendAsync(nameof(IMareHub.Client_UserUpdateProfile), new UserDto(new(split[1]))).ConfigureAwait(false); + + await arg.Message.ModifyAsync(msg => + { + msg.Content = arg.Message.Content; + msg.Components = null; + msg.Embed = new Optional(builder.Build()); + }).ConfigureAwait(false); + } + + private async Task DiscordClient_Ready() + { + var guild = (await _discordClient.Rest.GetGuildsAsync()).First(); + await _interactionModule.RegisterCommandsToGuildAsync(guild.Id, true).ConfigureAwait(false); + + //_ = RemoveUsersNotInVanityRole(); + _ = ProcessReportsQueue(); + } + + private Task Log(LogMessage msg) + { + _logger.LogInformation("{msg}", msg); + + return Task.CompletedTask; + } + + private async Task ProcessReportsQueue() + { + var guild = (await _discordClient.Rest.GetGuildsAsync()).First(); + + _processReportQueueCts?.Cancel(); + _processReportQueueCts?.Dispose(); + _processReportQueueCts = new(); + var token = _processReportQueueCts.Token; + while (!token.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromSeconds(30)).ConfigureAwait(false); + + if (_discordClient.ConnectionState != ConnectionState.Connected) continue; + var reportChannelId = _configurationService.GetValue(nameof(ServicesConfiguration.DiscordChannelForReports)); + if (reportChannelId == null) continue; + + try + { + using (var scope = _services.CreateScope()) + { + _logger.LogTrace("Checking for Profile Reports"); + var dbContext = scope.ServiceProvider.GetRequiredService(); + if (!dbContext.UserProfileReports.Any()) + { + continue; + } + + var reports = await dbContext.UserProfileReports.ToListAsync().ConfigureAwait(false); + var restChannel = await guild.GetTextChannelAsync(reportChannelId.Value).ConfigureAwait(false); + + foreach (var report in reports) + { + var reportedUser = await dbContext.Users.SingleAsync(u => u.UID == report.ReportedUserUID).ConfigureAwait(false); + var reportedUserLodestone = await dbContext.LodeStoneAuth.SingleOrDefaultAsync(u => u.User.UID == report.ReportedUserUID).ConfigureAwait(false); + var reportingUser = await dbContext.Users.SingleAsync(u => u.UID == report.ReportingUserUID).ConfigureAwait(false); + var reportingUserLodestone = await dbContext.LodeStoneAuth.SingleOrDefaultAsync(u => u.User.UID == report.ReportingUserUID).ConfigureAwait(false); + var reportedUserProfile = await dbContext.UserProfileData.SingleAsync(u => u.UserUID == report.ReportedUserUID).ConfigureAwait(false); + EmbedBuilder eb = new(); + eb.WithTitle("Mare Synchronos Profile Report"); + + StringBuilder reportedUserSb = new(); + StringBuilder reportingUserSb = new(); + reportedUserSb.Append(reportedUser.UID); + reportingUserSb.Append(reportingUser.UID); + if (reportedUserLodestone != null) + { + reportedUserSb.AppendLine($" (<@{reportedUserLodestone.DiscordId}>)"); + } + if (reportingUserLodestone != null) + { + reportingUserSb.AppendLine($" (<@{reportingUserLodestone.DiscordId}>)"); + } + eb.AddField("Reported User", reportedUserSb.ToString()); + eb.AddField("Reporting User", reportingUserSb.ToString()); + eb.AddField("Report Date (UTC)", report.ReportDate); + eb.AddField("Report Reason", string.IsNullOrWhiteSpace(report.ReportReason) ? "-" : report.ReportReason); + eb.AddField("Reported User Profile Description", string.IsNullOrWhiteSpace(reportedUserProfile.UserDescription) ? "-" : reportedUserProfile.UserDescription); + eb.AddField("Reported User Profile Is NSFW", reportedUserProfile.IsNSFW); + + var cb = new ComponentBuilder(); + cb.WithButton("Dismiss Report", customId: $"mare-report-button-dismiss-{reportedUser.UID}", style: ButtonStyle.Primary); + cb.WithButton("Ban profile", customId: $"mare-report-button-banprofile-{reportedUser.UID}", style: ButtonStyle.Secondary); + cb.WithButton("Ban user", customId: $"mare-report-button-banuser-{reportedUser.UID}", style: ButtonStyle.Danger); + cb.WithButton("Dismiss and Ban Reporting user", customId: $"mare-report-button-banreporting-{reportedUser.UID}-{reportingUser.UID}", style: ButtonStyle.Danger); + + if (!string.IsNullOrEmpty(reportedUserProfile.Base64ProfileImage)) + { + var fileName = reportedUser.UID + "_profile_" + Guid.NewGuid().ToString("N") + ".png"; + eb.WithImageUrl($"attachment://{fileName}"); + using MemoryStream ms = new(Convert.FromBase64String(reportedUserProfile.Base64ProfileImage)); + await restChannel.SendFileAsync(ms, fileName, "User Report", embed: eb.Build(), components: cb.Build(), isSpoiler: true).ConfigureAwait(false); + } + else + { + var msg = await restChannel.SendMessageAsync(embed: eb.Build(), components: cb.Build()).ConfigureAwait(false); + } + + dbContext.Remove(report); + } + + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to process reports"); + } + } + } + + private async Task RemoveUsersNotInVanityRole() + { + _vanityUpdateCts?.Cancel(); + _vanityUpdateCts?.Dispose(); + _vanityUpdateCts = new(); + var token = _vanityUpdateCts.Token; + var guild = (await _discordClient.Rest.GetGuildsAsync()).First(); + var commands = await guild.GetApplicationCommandsAsync(); + var appId = await _discordClient.GetApplicationInfoAsync().ConfigureAwait(false); + var vanityCommandId = commands.First(c => c.ApplicationId == appId.Id && c.Name == "setvanityuid").Id; + + while (!token.IsCancellationRequested) + { + try + { + _logger.LogInformation($"Cleaning up Vanity UIDs"); + _logger.LogInformation("Getting application commands from guild {guildName}", guild.Name); + var restGuild = await _discordClient.Rest.GetGuildAsync(guild.Id); + var vanityCommand = await restGuild.GetSlashCommandAsync(vanityCommandId).ConfigureAwait(false); + GuildApplicationCommandPermission commandPermissions = null; + try + { + _logger.LogInformation($"Getting command permissions"); + commandPermissions = await vanityCommand.GetCommandPermission().ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting command permissions"); + throw new Exception("Can't get command permissions"); + } + + _logger.LogInformation($"Getting allowed role ids from permissions"); + List allowedRoleIds = new(); + try + { + allowedRoleIds = (from perm in commandPermissions.Permissions where perm.TargetType == ApplicationCommandPermissionTarget.Role where perm.Permission select perm.TargetId).ToList(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error resolving permissions to roles"); + } + + _logger.LogInformation($"Found allowed role ids: {string.Join(", ", allowedRoleIds)}"); + + if (allowedRoleIds.Any()) + { + await using var scope = _services.CreateAsyncScope(); + await using (var db = scope.ServiceProvider.GetRequiredService()) + { + var aliasedUsers = await db.LodeStoneAuth.Include("User") + .Where(c => c.User != null && !string.IsNullOrEmpty(c.User.Alias)).ToListAsync().ConfigureAwait(false); + var aliasedGroups = await db.Groups.Include(u => u.Owner) + .Where(c => !string.IsNullOrEmpty(c.Alias)).ToListAsync().ConfigureAwait(false); + + foreach (var lodestoneAuth in aliasedUsers) + { + var discordUser = await restGuild.GetUserAsync(lodestoneAuth.DiscordId).ConfigureAwait(false); + _logger.LogInformation($"Checking User: {lodestoneAuth.DiscordId}, {lodestoneAuth.User.UID} ({lodestoneAuth.User.Alias}), User in Roles: {string.Join(", ", discordUser?.RoleIds ?? new List())}"); + + if (discordUser == null || !discordUser.RoleIds.Any(u => allowedRoleIds.Contains(u))) + { + _logger.LogInformation($"User {lodestoneAuth.User.UID} not in allowed roles, deleting alias"); + lodestoneAuth.User.Alias = null; + var secondaryUsers = await db.Auth.Include(u => u.User).Where(u => u.PrimaryUserUID == lodestoneAuth.User.UID).ToListAsync().ConfigureAwait(false); + foreach (var secondaryUser in secondaryUsers) + { + _logger.LogInformation($"Secondary User {secondaryUser.User.UID} not in allowed roles, deleting alias"); + + secondaryUser.User.Alias = null; + db.Update(secondaryUser.User); + } + db.Update(lodestoneAuth.User); + } + + await db.SaveChangesAsync().ConfigureAwait(false); + await Task.Delay(1000); + } + + foreach (var group in aliasedGroups) + { + var lodestoneUser = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(f => f.User.UID == group.OwnerUID).ConfigureAwait(false); + RestGuildUser discordUser = null; + if (lodestoneUser != null) + { + discordUser = await restGuild.GetUserAsync(lodestoneUser.DiscordId).ConfigureAwait(false); + } + + _logger.LogInformation($"Checking Group: {group.GID}, owned by {lodestoneUser?.User?.UID ?? string.Empty} ({lodestoneUser?.User?.Alias ?? string.Empty}), User in Roles: {string.Join(", ", discordUser?.RoleIds ?? new List())}"); + + if (lodestoneUser == null || discordUser == null || !discordUser.RoleIds.Any(u => allowedRoleIds.Contains(u))) + { + _logger.LogInformation($"User {lodestoneUser.User.UID} not in allowed roles, deleting group alias"); + group.Alias = null; + db.Update(group); + } + + await db.SaveChangesAsync().ConfigureAwait(false); + await Task.Delay(1000); + } + } + } + else + { + _logger.LogInformation("No roles for command defined, no cleanup performed"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Something failed during checking vanity user uids"); + } + + _logger.LogInformation("Vanity UID cleanup complete"); + await Task.Delay(TimeSpan.FromHours(12), _vanityUpdateCts.Token).ConfigureAwait(false); + } + } + + private async Task UpdateStatusAsync() + { + _updateStatusCts = new(); + while (!_updateStatusCts.IsCancellationRequested) + { + var endPoint = _connectionMultiplexer.GetEndPoints().First(); + var onlineUsers = await _connectionMultiplexer.GetServer(endPoint).KeysAsync(pattern: "UID:*").CountAsync(); + + //_logger.LogInformation("Users online: " + onlineUsers); + await _discordClient.SetActivityAsync(new Game("with " + onlineUsers + " Users")).ConfigureAwait(false); + await Task.Delay(TimeSpan.FromSeconds(15)).ConfigureAwait(false); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBotServices.cs b/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBotServices.cs new file mode 100644 index 0000000..7c8cc94 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBotServices.cs @@ -0,0 +1,61 @@ +using System.Collections.Concurrent; +using MareSynchronosShared.Metrics; + +namespace MareSynchronosServices.Discord; + +public class DiscordBotServices +{ + public readonly string[] LodestoneServers = new[] { "eu", "na", "jp", "fr", "de" }; + public ConcurrentDictionary DiscordLodestoneMapping = new(); + public ConcurrentDictionary DiscordRelinkLodestoneMapping = new(); + public ConcurrentDictionary LastVanityChange = new(); + public ConcurrentDictionary LastVanityGidChange = new(); + private readonly IServiceProvider _serviceProvider; + private CancellationTokenSource? verificationTaskCts; + + public DiscordBotServices(IServiceProvider serviceProvider, ILogger logger, MareMetrics metrics) + { + _serviceProvider = serviceProvider; + Logger = logger; + Metrics = metrics; + } + + public ILogger Logger { get; init; } + public MareMetrics Metrics { get; init; } + public ConcurrentQueue>> VerificationQueue { get; } = new(); + + public Task Start() + { + _ = ProcessVerificationQueue(); + return Task.CompletedTask; + } + + public Task Stop() + { + verificationTaskCts?.Cancel(); + return Task.CompletedTask; + } + + private async Task ProcessVerificationQueue() + { + verificationTaskCts = new CancellationTokenSource(); + while (!verificationTaskCts.IsCancellationRequested) + { + if (VerificationQueue.TryDequeue(out var queueitem)) + { + try + { + queueitem.Value.Invoke(_serviceProvider); + + Logger.LogInformation("Sent login information to user"); + } + catch (Exception e) + { + Logger.LogError(e, "Error during queue work"); + } + } + + await Task.Delay(TimeSpan.FromSeconds(2), verificationTaskCts.Token).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServices/Discord/MareModule.cs b/MareSynchronosServer/MareSynchronosServices/Discord/MareModule.cs new file mode 100644 index 0000000..23adb85 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/Discord/MareModule.cs @@ -0,0 +1,1260 @@ +using Discord; +using Discord.Interactions; +using MareSynchronosShared.Data; +using System.Text.RegularExpressions; +using Microsoft.EntityFrameworkCore; +using Discord.WebSocket; +using Prometheus; +using MareSynchronosShared.Models; +using MareSynchronosShared.Utils; +using MareSynchronosShared.Services; +using StackExchange.Redis; +using MareSynchronos.API.Data.Enum; +using System.Net.Http.Headers; +using MareSynchronosShared.Utils.Configuration; + +namespace MareSynchronosServices.Discord; + +public class LodestoneModal : IModal +{ + public string Title => "Verify with Lodestone"; + + [InputLabel("Enter the Lodestone URL of your Character")] + [ModalTextInput("lodestone_url", TextInputStyle.Short, "https://*.finalfantasyxiv.com/lodestone/character//")] + public string LodestoneUrl { get; set; } +} + +public class MareModule : InteractionModuleBase +{ + private readonly ILogger _logger; + private readonly IServiceProvider _services; + private readonly DiscordBotServices _botServices; + private readonly IConfigurationService _mareClientConfigurationService; + private readonly IConfigurationService _mareServicesConfiguration; + private readonly IConnectionMultiplexer _connectionMultiplexer; + private readonly ServerTokenGenerator _serverTokenGenerator; + private Random random = new(); + + public MareModule(ILogger logger, IServiceProvider services, DiscordBotServices botServices, + IConfigurationService mareClientConfigurationService, + IConfigurationService mareServicesConfiguration, + IConnectionMultiplexer connectionMultiplexer, ServerTokenGenerator serverTokenGenerator) + { + _logger = logger; + _services = services; + _botServices = botServices; + _mareClientConfigurationService = mareClientConfigurationService; + _mareServicesConfiguration = mareServicesConfiguration; + _connectionMultiplexer = connectionMultiplexer; + _serverTokenGenerator = serverTokenGenerator; + } + + //[SlashCommand("register", "Starts the registration process")] + public async Task Register([Summary("overwrite", "Overwrites your old account")] bool overwrite = false) + { + _logger.LogInformation("SlashCommand:{userId}:{Method}:{params}", + Context.Interaction.User.Id, nameof(Register), + string.Join(",", new[] { $"{nameof(overwrite)}:{overwrite}" })); + + try + { + if (overwrite) + { + await DeletePreviousUserAccount(Context.User.Id).ConfigureAwait(false); + } + + using var scope = _services.CreateScope(); + using var db = scope.ServiceProvider.GetService(); + + if (db.LodeStoneAuth.Any(a => a.DiscordId == Context.User.Id)) + { + EmbedBuilder eb = new(); + // user already in db + eb.WithTitle("Registration failed"); + eb.WithDescription("You are already registered. Use `/recover overwrite` to delete your old account and create a new one."); + await RespondAsync(embeds: new Embed[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + else + { + // Loporrit - Register immediately + var user = new User(); + + var hasValidUid = false; + while (!hasValidUid) + { + var uid = StringUtils.GenerateRandomString(7); + if (db.Users.Any(u => u.UID == uid || u.Alias == uid)) continue; + user.UID = uid; + hasValidUid = true; + } + + user.LastLoggedIn = DateTime.UtcNow; + + var computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString()); + + var auth = new Auth() + { + HashedKey = StringUtils.Sha256String(computedHash), + User = user, + }; + + LodeStoneAuth lsAuth = new LodeStoneAuth() + { + DiscordId = Context.User.Id, + HashedLodestoneId = null, + LodestoneAuthString = null, + User = user, + StartedAt = null + }; + + await db.Users.AddAsync(user).ConfigureAwait(false); + await db.Auth.AddAsync(auth).ConfigureAwait(false); + await db.LodeStoneAuth.AddAsync(lsAuth).ConfigureAwait(false); + await db.SaveChangesAsync().ConfigureAwait(false); + + _botServices.Logger.LogInformation("User registered: {userUID}", user.UID); + + EmbedBuilder eb = new(); + + eb.WithTitle("Registration successful"); + eb.WithDescription("This is your private secret key. Do not share this private secret key with anyone. **If you lose it, it is irrevocably lost.**" + + Environment.NewLine + Environment.NewLine + + $"**{computedHash}**" + + Environment.NewLine + Environment.NewLine + + "Enter this key in Snowcloak and hit Connect / Reconnect." + + Environment.NewLine + + "Have fun."); + + await RespondAsync(embeds: new Embed[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + } + catch (Exception ex) + { + EmbedBuilder eb = new(); + eb.WithTitle("An error occured"); + eb.WithDescription("Please report this error to bug-reports: " + Environment.NewLine + ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine); + + await RespondAsync(embeds: new Embed[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + } + + //[SlashCommand("createsecondaryuid", "Creates a new Secret Key to be used for alts")] + public async Task AddSecondary() + { + try + { + var embed = await HandleAddSecondary(Context.User.Id).ConfigureAwait(false); + await RespondAsync(embeds: new[] { embed }, ephemeral: true).ConfigureAwait(false); + } + catch (Exception ex) + { + EmbedBuilder eb = new(); + eb.WithTitle("An error occured"); + eb.WithDescription("Please report this error to bug-reports: " + Environment.NewLine + ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine); + + await RespondAsync(embeds: new Embed[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + } + + //[SlashCommand("setvanityuid", "Sets your Vanity UID.")] + public async Task SetVanityUid([Summary("vanity_uid", "Desired Vanity UID")] string vanityUid, + [Summary("secondary_uid", "Will set the vanity UID for a secondary UID")] string? secondaryUid = null) + { + _logger.LogInformation("SlashCommand:{userId}:{Method}:{params}", + Context.Interaction.User.Id, nameof(SetVanityUid), + string.Join(",", new[] { $"{nameof(vanityUid)}:{vanityUid}" })); + + try + { + EmbedBuilder eb = new(); + + eb = await HandleVanityUid(eb, Context.User.Id, vanityUid, secondaryUid); + + await RespondAsync(embeds: new[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + catch (Exception ex) + { + EmbedBuilder eb = new(); + eb.WithTitle("An error occured"); + eb.WithDescription("Please report this error to bug-reports: " + Environment.NewLine + ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine); + + await RespondAsync(embeds: new Embed[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + } + + //[SlashCommand("setsyncshellvanityid", "Sets a Vanity GID for a Syncshell")] + public async Task SetSyncshellVanityId( + [Summary("syncshell_id", "Syncshell ID")] string syncshellId, + [Summary("vanity_syncshell_id", "Desired Vanity Syncshell ID")] string vanityId) + { + _logger.LogInformation("SlashCommand:{userId}:{Method}:{params}", + Context.Interaction.User.Id, nameof(SetSyncshellVanityId), + string.Join(",", new[] { $"{nameof(syncshellId)}:{syncshellId}", $"{nameof(vanityId)}:{vanityId}" })); + + try + { + EmbedBuilder eb = new(); + + eb = await HandleVanityGid(eb, Context.User.Id, syncshellId, vanityId); + + await RespondAsync(embeds: new[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + catch (Exception ex) + { + EmbedBuilder eb = new(); + eb.WithTitle("An error occured"); + eb.WithDescription("Please report this error to bug-reports: " + Environment.NewLine + ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine); + + await RespondAsync(embeds: new Embed[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + } + + // Loporrit - Disable /verify command + //[SlashCommand("verify", "Finishes the registration process for the Mare Synchronos server of this Discord")] + public async Task Verify() + { + _logger.LogInformation("SlashCommand:{userId}:{Method}", + Context.Interaction.User.Id, nameof(Verify)); + try + { + EmbedBuilder eb = new(); + if (_botServices.VerificationQueue.Any(u => u.Key == Context.User.Id)) + { + eb.WithTitle("Already queued for verfication"); + eb.WithDescription("You are already queued for verification. Please wait."); + await RespondAsync(embeds: new[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + else if (!_botServices.DiscordLodestoneMapping.ContainsKey(Context.User.Id)) + { + eb.WithTitle("Cannot verify registration"); + eb.WithDescription("You need to **/register** first before you can **/verify**" + Environment.NewLine + "If your registration got stuck for some reason, use **/register overwrite:true**"); + await RespondAsync(embeds: new[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + else + { + await DeferAsync(ephemeral: true).ConfigureAwait(false); + _botServices.VerificationQueue.Enqueue(new KeyValuePair>(Context.User.Id, async (sp) => await HandleVerifyAsync((SocketSlashCommand)Context.Interaction, sp))); + } + } + catch (Exception ex) + { + EmbedBuilder eb = new(); + eb.WithTitle("An error occured"); + eb.WithDescription("Please report this error to bug-reports: " + Environment.NewLine + ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine); + + await RespondAsync(embeds: new Embed[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + } + + // Loporrit - Disable /verify command + //[SlashCommand("verify_relink", "Finishes the relink process for your user on the Mare Synchronos server of this Discord")] + public async Task VerifyRelink() + { + _logger.LogInformation("SlashCommand:{userId}:{Method}", + Context.Interaction.User.Id, nameof(VerifyRelink)); + + try + { + EmbedBuilder eb = new(); + if (_botServices.VerificationQueue.Any(u => u.Key == Context.User.Id)) + { + eb.WithTitle("Already queued for verfication"); + eb.WithDescription("You are already queued for verification. Please wait."); + await RespondAsync(embeds: new[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + else if (!_botServices.DiscordRelinkLodestoneMapping.ContainsKey(Context.User.Id)) + { + eb.WithTitle("Cannot verify relink"); + eb.WithDescription("You need to **/relink** first before you can **/verify_relink**"); + await RespondAsync(embeds: new[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + else + { + await DeferAsync(ephemeral: true).ConfigureAwait(false); + _botServices.VerificationQueue.Enqueue(new KeyValuePair>(Context.User.Id, async (sp) => await HandleVerifyRelinkAsync((SocketSlashCommand)Context.Interaction, sp))); + } + } + catch (Exception ex) + { + EmbedBuilder eb = new(); + eb.WithTitle("An error occured"); + eb.WithDescription("Please report this error to bug-reports: " + Environment.NewLine + ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine); + + await RespondAsync(embeds: new Embed[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + + } + + //[SlashCommand("recover", "Allows you to recover your account by generating a new secret key")] + public async Task Recover([Summary("secondary_uid", "(Optional) Your secondary UID")] string? secondaryUid = null) + { + _logger.LogInformation("SlashCommand:{userId}:{Method}", + Context.Interaction.User.Id, nameof(Recover)); + + await RespondWithModalAsync($"recover_modal:{secondaryUid ?? "-1"}").ConfigureAwait(false); + } + + [SlashCommand("userinfo", "Shows you your user information")] + public async Task UserInfo([Summary("secondary_uid", "(Optional) Your secondary UID")] string? secondaryUid = null, + [Summary("discord_user", "ADMIN ONLY: Discord User to check for")] IUser? discordUser = null, + [Summary("uid", "ADMIN ONLY: UID to check for")] string? uid = null) + { + _logger.LogInformation("SlashCommand:{userId}:{Method}", + Context.Interaction.User.Id, nameof(UserInfo)); + + try + { + EmbedBuilder eb = new(); + + eb = await HandleUserInfo(eb, Context.User.Id, secondaryUid, discordUser?.Id ?? null, uid); + + await RespondAsync(embeds: new[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + catch (Exception ex) + { + EmbedBuilder eb = new(); + eb.WithTitle("An error occured"); + eb.WithDescription("Please report this error to bug-reports: " + Environment.NewLine + ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine); + + await RespondAsync(embeds: new Embed[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + } + + //[SlashCommand("relink", "Allows you to link a new Discord account to an existing Mare account")] + public async Task Relink() + { + _logger.LogInformation("SlashCommand:{userId}:{Method}", + Context.Interaction.User.Id, nameof(Relink)); + await RespondWithModalAsync("relink_modal").ConfigureAwait(false); + } + + [SlashCommand("useradd", "ADMIN ONLY: add a user unconditionally to the Database")] + public async Task UserAdd([Summary("desired_uid", "Desired UID")] string desiredUid) + { + _logger.LogInformation("SlashCommand:{userId}:{Method}:{params}", + Context.Interaction.User.Id, nameof(UserAdd), + string.Join(",", new[] { $"{nameof(desiredUid)}:{desiredUid}" })); + + try + { + var embed = await HandleUserAdd(desiredUid, Context.User.Id); + + await RespondAsync(embeds: new[] { embed }, ephemeral: true).ConfigureAwait(false); + } + catch (Exception ex) + { + EmbedBuilder eb = new(); + eb.WithTitle("An error occured"); + eb.WithDescription("Please report this error to bug-reports: " + Environment.NewLine + ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine); + + await RespondAsync(embeds: new Embed[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + } + + [SlashCommand("message", "ADMIN ONLY: sends a message to clients")] + public async Task SendMessageToClients([Summary("message", "Message to send")] string message, + [Summary("severity", "Severity of the message")] MessageSeverity messageType = MessageSeverity.Information, + [Summary("uid", "User ID to the person to send the message to")] string? uid = null) + { + _logger.LogInformation("SlashCommand:{userId}:{Method}:{message}:{type}:{uid}", Context.Interaction.User.Id, nameof(SendMessageToClients), message, messageType, uid); + + using var scope = _services.CreateScope(); + using var db = scope.ServiceProvider.GetService(); + + if (!(await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(a => a.DiscordId == Context.Interaction.User.Id))?.User?.IsAdmin ?? true) + { + await RespondAsync("No permission", ephemeral: true).ConfigureAwait(false); + return; + } + + if (!string.IsNullOrEmpty(uid) && !await db.Users.AnyAsync(u => u.UID == uid)) + { + await RespondAsync("Specified UID does not exist", ephemeral: true).ConfigureAwait(false); + return; + } + + try + { + using HttpClient c = new HttpClient(); + c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _serverTokenGenerator.Token); + await c.PostAsJsonAsync(new Uri(_mareServicesConfiguration.GetValue + (nameof(ServicesConfiguration.MainServerAddress)), "/msgc/sendMessage"), new ClientMessage(messageType, message, uid ?? string.Empty)) + .ConfigureAwait(false); + + var discordChannelForMessages = _mareServicesConfiguration.GetValueOrDefault(nameof(ServicesConfiguration.DiscordChannelForMessages), null); + if (uid == null && discordChannelForMessages != null) + { + var discordChannel = await Context.Guild.GetChannelAsync(discordChannelForMessages.Value) as IMessageChannel; + if (discordChannel != null) + { + var embedColor = messageType switch + { + MessageSeverity.Information => Color.Blue, + MessageSeverity.Warning => new Color(255, 255, 0), + MessageSeverity.Error => Color.Red, + _ => Color.Blue + }; + + EmbedBuilder eb = new(); + eb.WithTitle(messageType + " server message"); + eb.WithColor(embedColor); + eb.WithDescription(message); + + await discordChannel.SendMessageAsync(embed: eb.Build()); + } + } + + await RespondAsync("Message sent", ephemeral: true).ConfigureAwait(false); + } + catch (Exception ex) + { + await RespondAsync("Failed to send message: " + ex.ToString(), ephemeral: true).ConfigureAwait(false); + } + } + + //[ModalInteraction("recover_modal:*")] + public async Task RecoverModal(string? secondaryUid, LodestoneModal modal) + { + _logger.LogInformation("Modal:{userId}:{Method}", + Context.Interaction.User.Id, nameof(RecoverModal)); + + if (secondaryUid == "-1") secondaryUid = null; + + try + { + var embed = await HandleRecoverModalAsync(modal, Context.User.Id, secondaryUid).ConfigureAwait(false); + await RespondAsync(embeds: new Embed[] { embed }, ephemeral: true).ConfigureAwait(false); + } + catch (Exception ex) + { + EmbedBuilder eb = new(); + eb.WithTitle("An error occured"); + eb.WithDescription("Please report this error to bug-reports: " + Environment.NewLine + ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine); + + await RespondAsync(embeds: new Embed[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + } + + //[ModalInteraction("register_modal")] + public async Task RegisterModal(LodestoneModal modal) + { + _logger.LogInformation("Modal:{userId}:{Method}", + Context.Interaction.User.Id, nameof(RegisterModal)); + + try + { + var embed = await HandleRegisterModalAsync(modal, Context.User.Id).ConfigureAwait(false); + await RespondAsync(embeds: new Embed[] { embed }, ephemeral: true).ConfigureAwait(false); + } + catch (Exception ex) + { + EmbedBuilder eb = new(); + eb.WithTitle("An error occured"); + eb.WithDescription("Please report this error to bug-reports: " + Environment.NewLine + ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine); + + await RespondAsync(embeds: new Embed[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + } + + //[ModalInteraction("relink_modal")] + public async Task RelinkModal(LodestoneModal modal) + { + _logger.LogInformation("Modal:{userId}:{Method}", + Context.Interaction.User.Id, nameof(RelinkModal)); + + try + { + var embed = await HandleRelinkModalAsync(modal, Context.User.Id).ConfigureAwait(false); + await RespondAsync(embeds: new Embed[] { embed }, ephemeral: true).ConfigureAwait(false); + } + catch (Exception ex) + { + EmbedBuilder eb = new(); + eb.WithTitle("An error occured"); + eb.WithDescription("Please report this error to bug-reports: " + Environment.NewLine + ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine); + + await RespondAsync(embeds: new Embed[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + } + + public async Task HandleAddSecondary(ulong discordUserId) + { + var embed = new EmbedBuilder(); + + using var scope = _services.CreateScope(); + using var db = scope.ServiceProvider.GetService(); + + var lodestoneAuth = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(a => a.DiscordId == discordUserId).ConfigureAwait(false); + if (lodestoneAuth == null) + { + embed.WithTitle("Failed to add secondary user"); + embed.WithDescription("You have no registered account yet. Register an account first before trying to add secondary keys."); + return embed.Build(); + } + + var secondaryCount = await db.Auth.CountAsync(u => u.PrimaryUserUID == lodestoneAuth.User.UID).ConfigureAwait(false); + if (await db.Auth.CountAsync(u => u.PrimaryUserUID == lodestoneAuth.User.UID).ConfigureAwait(false) >= 25) + { + embed.WithTitle("Failed to add secondary user"); + embed.WithDescription("You already made 25 secondary UIDs, which is the limit."); + return embed.Build(); + } + + User newUser = new() + { + IsAdmin = false, + IsModerator = false, + LastLoggedIn = DateTime.UtcNow, + }; + + var hasValidUid = false; + while (!hasValidUid) + { + var uid = StringUtils.GenerateRandomString(7); + if (await db.Users.AnyAsync(u => u.UID == uid || u.Alias == uid).ConfigureAwait(false)) continue; + newUser.UID = uid; + hasValidUid = true; + } + + var computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString()); + var auth = new Auth() + { + HashedKey = StringUtils.Sha256String(computedHash), + User = newUser, + PrimaryUserUID = lodestoneAuth.User.UID, + UserUID = newUser.UID + }; + + await db.Users.AddAsync(newUser).ConfigureAwait(false); + await db.Auth.AddAsync(auth).ConfigureAwait(false); + + await db.SaveChangesAsync().ConfigureAwait(false); + + embed.WithTitle("Secondary UID created"); + embed.AddField("UID", newUser.UID); + embed.AddField("Secret Key", computedHash); + embed.AddField("Secondary UIDs", $"You now have {secondaryCount + 1}/25 secondary UIDs"); + + return embed.Build(); + } + + public async Task HandleUserAdd(string desiredUid, ulong discordUserId) + { + var embed = new EmbedBuilder(); + + using var scope = _services.CreateScope(); + using var db = scope.ServiceProvider.GetService(); + if (!(await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(a => a.DiscordId == discordUserId))?.User?.IsAdmin ?? true) + { + embed.WithTitle("Failed to add user"); + embed.WithDescription("No permission"); + } + else if (db.Users.Any(u => u.UID == desiredUid || u.Alias == desiredUid)) + { + embed.WithTitle("Failed to add user"); + embed.WithDescription("Already in Database"); + } + else + { + User newUser = new() + { + IsAdmin = false, + IsModerator = false, + LastLoggedIn = DateTime.UtcNow, + UID = desiredUid, + }; + + var computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString()); + var auth = new Auth() + { + HashedKey = StringUtils.Sha256String(computedHash), + User = newUser, + }; + + await db.Users.AddAsync(newUser); + await db.Auth.AddAsync(auth); + + await db.SaveChangesAsync(); + + embed.WithTitle("Successfully added " + desiredUid); + embed.WithDescription("Secret Key: " + computedHash); + } + + return embed.Build(); + } + + private async Task HandleUserInfo(EmbedBuilder eb, ulong id, string? secondaryUserUid = null, ulong? optionalUser = null, string? uid = null) + { + bool showForSecondaryUser = secondaryUserUid != null; + using var scope = _services.CreateScope(); + await using var db = scope.ServiceProvider.GetRequiredService(); + + var primaryUser = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.DiscordId == id).ConfigureAwait(false); + + ulong userToCheckForDiscordId = id; + + if (primaryUser == null) + { + eb.WithTitle("No account"); + eb.WithDescription("No account was found associated to your Discord user"); + return eb; + } + + bool isAdminCall = primaryUser.User.IsModerator || primaryUser.User.IsAdmin; + + if ((optionalUser != null || uid != null) && !isAdminCall) + { + eb.WithTitle("Unauthorized"); + eb.WithDescription("You are not authorized to view another users' information"); + return eb; + } + else if ((optionalUser != null || uid != null) && isAdminCall) + { + LodeStoneAuth userInDb = null; + if (optionalUser != null) + { + userInDb = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.DiscordId == optionalUser).ConfigureAwait(false); + } + else if (uid != null) + { + userInDb = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.User.UID == uid || u.User.Alias == uid).ConfigureAwait(false); + } + + if (userInDb == null) + { + eb.WithTitle("No account"); + eb.WithDescription("The Discord user has no valid account"); + return eb; + } + + userToCheckForDiscordId = userInDb.DiscordId; + } + + var lodestoneUser = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.DiscordId == userToCheckForDiscordId).ConfigureAwait(false); + var dbUser = lodestoneUser.User; + if (showForSecondaryUser) + { + dbUser = (await db.Auth.Include(u => u.User).SingleOrDefaultAsync(u => u.PrimaryUserUID == dbUser.UID && u.UserUID == secondaryUserUid))?.User; + if (dbUser == null) + { + eb.WithTitle("No such secondary UID"); + eb.WithDescription($"A secondary UID {secondaryUserUid} was not found attached to your primary UID {primaryUser.User.UID}."); + return eb; + } + } + + var auth = await db.Auth.Include(u => u.PrimaryUser).SingleOrDefaultAsync(u => u.UserUID == dbUser.UID).ConfigureAwait(false); + var groups = await db.Groups.Where(g => g.OwnerUID == dbUser.UID).ToListAsync().ConfigureAwait(false); + var groupsJoined = await db.GroupPairs.Where(g => g.GroupUserUID == dbUser.UID).ToListAsync().ConfigureAwait(false); + var identity = await _connectionMultiplexer.GetDatabase().StringGetAsync("UID:" + dbUser.UID).ConfigureAwait(false); + + eb.WithTitle("User Information"); + eb.WithDescription("This is the user information for Discord User <@" + userToCheckForDiscordId + ">" + Environment.NewLine + Environment.NewLine + + "If you want to verify your secret key is valid, go to https://emn178.github.io/online-tools/sha256.html and copy your secret key into there and compare it to the Hashed Secret Key provided below."); + eb.AddField("UID", dbUser.UID); + if (!string.IsNullOrEmpty(dbUser.Alias)) + { + eb.AddField("Vanity UID", dbUser.Alias); + } + if (showForSecondaryUser) + { + eb.AddField("Primary UID for " + dbUser.UID, auth.PrimaryUserUID); + } + else + { + var secondaryUIDs = await db.Auth.Where(p => p.PrimaryUserUID == dbUser.UID).Select(p => p.UserUID).ToListAsync(); + if (secondaryUIDs.Any()) + { + eb.AddField("Secondary UIDs", string.Join(Environment.NewLine, secondaryUIDs)); + } + } + eb.AddField("Last Online (UTC)", dbUser.LastLoggedIn.ToString("U")); + eb.AddField("Currently online ", !string.IsNullOrEmpty(identity)); + eb.AddField("Hashed Secret Key", auth.HashedKey); + eb.AddField("Joined Syncshells", groupsJoined.Count); + eb.AddField("Owned Syncshells", groups.Count); + foreach (var group in groups) + { + var syncShellUserCount = await db.GroupPairs.CountAsync(g => g.GroupGID == group.GID).ConfigureAwait(false); + if (!string.IsNullOrEmpty(group.Alias)) + { + eb.AddField("Owned Syncshell " + group.GID + " Vanity ID", group.Alias); + } + eb.AddField("Owned Syncshell " + group.GID + " User Count", syncShellUserCount); + } + + if (isAdminCall && !string.IsNullOrEmpty(identity)) + { + eb.AddField("Character Ident", identity); + } + + return eb; + } + + private async Task HandleRecoverModalAsync(LodestoneModal arg, ulong userid, string? secondaryUid) + { + var embed = new EmbedBuilder(); + + var lodestoneId = ParseCharacterIdFromLodestoneUrl(arg.LodestoneUrl); + if (lodestoneId == null) + { + embed.WithTitle("Invalid Lodestone URL"); + embed.WithDescription("The lodestone URL was not valid. It should have following format:" + Environment.NewLine + + "https://eu.finalfantasyxiv.com/lodestone/character/YOUR_LODESTONE_ID/"); + } + else + { + using var scope = _services.CreateScope(); + + var hashedLodestoneId = StringUtils.Sha256String(lodestoneId.ToString()); + + await using var db = scope.ServiceProvider.GetService(); + var existingLodestoneAuth = await db.LodeStoneAuth.Include("User") + .FirstOrDefaultAsync(a => a.DiscordId == userid && a.HashedLodestoneId == hashedLodestoneId) + .ConfigureAwait(false); + + if (existingLodestoneAuth == null || existingLodestoneAuth.User == null) + { + embed.WithTitle("Recovery failed"); + embed.WithDescription("This DiscordID or Lodestone account pair does not exist."); + } + else + { + string computedHash = string.Empty; + Auth auth; + if (string.IsNullOrEmpty(secondaryUid)) + { + var previousAuth = await db.Auth.FirstOrDefaultAsync(u => u.UserUID == existingLodestoneAuth.User.UID); + if (previousAuth != null) + { + db.Auth.Remove(previousAuth); + } + + computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString()); + auth = new Auth() + { + HashedKey = StringUtils.Sha256String(computedHash), + User = existingLodestoneAuth.User, + }; + + await db.Auth.AddAsync(auth).ConfigureAwait(false); + } + else + { + var previousAuth = await db.Auth.Include(u => u.User).FirstOrDefaultAsync(u => u.PrimaryUserUID == existingLodestoneAuth.User.UID && u.UserUID == secondaryUid).ConfigureAwait(false); + if (previousAuth == null) + { + embed.WithTitle("Recovery failed"); + embed.WithDescription("This DiscordID has no secondary UID " + secondaryUid); + + return embed.Build(); + } + + db.Auth.Remove(previousAuth); + + computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString()); + auth = new Auth() + { + HashedKey = StringUtils.Sha256String(computedHash), + User = previousAuth.User, + PrimaryUserUID = existingLodestoneAuth.User.UID + }; + } + + embed.WithTitle("Recovery successful"); + embed.WithDescription("This is your new private secret key. Do not share this private secret key with anyone. **If you lose it, it is irrevocably lost.**" + + Environment.NewLine + Environment.NewLine + + $"**{computedHash}**" + + Environment.NewLine + Environment.NewLine + + "Enter this key in Snowcloak and hit save to connect to the service."); + + await db.Auth.AddAsync(auth).ConfigureAwait(false); + await db.SaveChangesAsync().ConfigureAwait(false); + } + } + + return embed.Build(); + } + + private async Task HandleRegisterModalAsync(LodestoneModal arg, ulong userid) + { + var embed = new EmbedBuilder(); + + var lodestoneId = ParseCharacterIdFromLodestoneUrl(arg.LodestoneUrl); + if (lodestoneId == null) + { + embed.WithTitle("Invalid Lodestone URL"); + embed.WithDescription("The lodestone URL was not valid. It should have following format:" + Environment.NewLine + + "https://eu.finalfantasyxiv.com/lodestone/character/YOUR_LODESTONE_ID/"); + } + else + { + // check if userid is already in db + using var scope = _services.CreateScope(); + + var hashedLodestoneId = StringUtils.Sha256String(lodestoneId.ToString()); + + using var db = scope.ServiceProvider.GetService(); + + // check if discord id or lodestone id is banned + if (db.BannedRegistrations.Any(a => a.DiscordIdOrLodestoneAuth == userid.ToString() || a.DiscordIdOrLodestoneAuth == hashedLodestoneId)) + { + embed.WithTitle("no"); + embed.WithDescription("your account is banned"); + } + else if (db.LodeStoneAuth.Any(a => a.DiscordId == userid)) + { + // user already in db + embed.WithTitle("Registration failed"); + embed.WithDescription("You cannot register more than one lodestone character to your discord account."); + } + else if (db.LodeStoneAuth.Any(a => a.HashedLodestoneId == hashedLodestoneId)) + { + // character already in db + embed.WithTitle("Registration failed"); + embed.WithDescription("This lodestone character already exists in the Database. If you want to attach this character to your current Discord account use **/relink**."); + } + else + { + string lodestoneAuth = await GenerateLodestoneAuth(userid, hashedLodestoneId, db).ConfigureAwait(false); + // check if lodestone id is already in db + embed.WithTitle("Authorize your character"); + embed.WithDescription("Add following key to your character profile at https://na.finalfantasyxiv.com/lodestone/my/setting/profile/" + + Environment.NewLine + Environment.NewLine + + $"**{lodestoneAuth}**" + + Environment.NewLine + Environment.NewLine + + $"**! THIS IS NOT THE KEY YOU HAVE TO ENTER IN MARE !**" + + Environment.NewLine + Environment.NewLine + + "Once added and saved, use command **/verify** to finish registration and receive a secret key to use for Mare Synchronos." + + Environment.NewLine + + "__You can delete the entry from your profile after verification.__" + + Environment.NewLine + Environment.NewLine + + "The verification will expire in approximately 15 minutes. If you fail to **/verify** the registration will be invalidated and you have to **/register** again."); + _botServices.DiscordLodestoneMapping[userid] = lodestoneId.ToString(); + } + } + + return embed.Build(); + } + + private async Task HandleRelinkModalAsync(LodestoneModal arg, ulong userid) + { + var embed = new EmbedBuilder(); + + var lodestoneId = ParseCharacterIdFromLodestoneUrl(arg.LodestoneUrl); + if (lodestoneId == null) + { + embed.WithTitle("Invalid Lodestone URL"); + embed.WithDescription("The lodestone URL was not valid. It should have following format:" + Environment.NewLine + + "https://eu.finalfantasyxiv.com/lodestone/character/YOUR_LODESTONE_ID/"); + } + else + { + // check if userid is already in db + using var scope = _services.CreateScope(); + + var hashedLodestoneId = StringUtils.Sha256String(lodestoneId.ToString()); + + using var db = scope.ServiceProvider.GetService(); + + // check if discord id or lodestone id is banned + if (db.BannedRegistrations.Any(a => a.DiscordIdOrLodestoneAuth == userid.ToString() || a.DiscordIdOrLodestoneAuth == hashedLodestoneId)) + { + embed.WithTitle("no"); + embed.WithDescription("your account is banned"); + } + else if (db.LodeStoneAuth.Any(a => a.DiscordId == userid)) + { + // user already in db + embed.WithTitle("Relink failed"); + embed.WithDescription("You cannot register more than one lodestone character to your discord account."); + } + else if (!db.LodeStoneAuth.Any(a => a.HashedLodestoneId == hashedLodestoneId)) + { + // character already in db + embed.WithTitle("Relink failed"); + embed.WithDescription("This lodestone character does not exist in the database."); + } + else + { + string lodestoneAuth = await GenerateLodestoneAuth(userid, hashedLodestoneId, db).ConfigureAwait(false); + // check if lodestone id is already in db + embed.WithTitle("Authorize your character for relinking"); + embed.WithDescription("Add following key to your character profile at https://na.finalfantasyxiv.com/lodestone/my/setting/profile/" + + Environment.NewLine + Environment.NewLine + + $"**{lodestoneAuth}**" + + Environment.NewLine + Environment.NewLine + + $"**! THIS IS NOT THE KEY YOU HAVE TO ENTER IN MARE !**" + + Environment.NewLine + Environment.NewLine + + "Once added and saved, use command **/verify_relink** to finish relink and receive a new secret key to use for Mare Synchronos." + + Environment.NewLine + + "__You can delete the entry from your profile after verification.__" + + Environment.NewLine + Environment.NewLine + + "The verification will expire in approximately 15 minutes. If you fail to **/verify_relink** the relink will be invalidated and you have to **/relink** again."); + _botServices.DiscordRelinkLodestoneMapping[userid] = lodestoneId.ToString(); + } + } + + return embed.Build(); + } + + private async Task GenerateLodestoneAuth(ulong discordid, string hashedLodestoneId, MareDbContext dbContext) + { + var auth = StringUtils.GenerateRandomString(32); + LodeStoneAuth lsAuth = new LodeStoneAuth() + { + DiscordId = discordid, + HashedLodestoneId = hashedLodestoneId, + LodestoneAuthString = auth, + StartedAt = DateTime.UtcNow + }; + + dbContext.Add(lsAuth); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + + return auth; + } + + private int? ParseCharacterIdFromLodestoneUrl(string lodestoneUrl) + { + var regex = new Regex(@"https:\/\/(na|eu|de|fr|jp)\.finalfantasyxiv\.com\/lodestone\/character\/\d+"); + var matches = regex.Match(lodestoneUrl); + var isLodestoneUrl = matches.Success; + if (!isLodestoneUrl || matches.Groups.Count < 1) return null; + + lodestoneUrl = matches.Groups[0].ToString(); + var stringId = lodestoneUrl.Split('/', StringSplitOptions.RemoveEmptyEntries).Last(); + if (!int.TryParse(stringId, out int lodestoneId)) + { + return null; + } + + return lodestoneId; + } + + private async Task HandleVanityUid(EmbedBuilder eb, ulong id, string newUid, string? secondaryUid) + { + if (_botServices.LastVanityChange.TryGetValue(id, out var lastChange)) + { + var timeRemaining = DateTime.UtcNow.Subtract(lastChange); + if (timeRemaining.TotalHours < 24) + { + eb.WithTitle(("Failed to set Vanity UID")); + eb.WithDescription( + $"You can only change your vanity UID once every 24h. Your last change is {timeRemaining} ago."); + } + } + + Regex rgx = new(@"^[_\-a-zA-Z0-9]{5,15}$", RegexOptions.ECMAScript); + if (!rgx.Match(newUid).Success || newUid.Length < 5 || newUid.Length > 15) + { + eb.WithTitle("Failed to set Vanity UID"); + eb.WithDescription("The Vanity UID must be between 5 and 15 characters and only contain letters A-Z, numbers 0-9, as well as - and _."); + return eb; + } + + using var scope = _services.CreateScope(); + await using var db = scope.ServiceProvider.GetRequiredService(); + + var lodestoneUser = await db.LodeStoneAuth.Include("User").SingleOrDefaultAsync(u => u.DiscordId == id).ConfigureAwait(false); + if (lodestoneUser == null) + { + eb.WithTitle("Failed to set Vanity UID"); + eb.WithDescription("You do not have a registered account on this server."); + return eb; + } + + var uidExists = await db.Users.AnyAsync(u => u.UID == newUid || u.Alias == newUid).ConfigureAwait(false); + if (uidExists) + { + eb.WithTitle("Failed to set Vanity UID"); + eb.WithDescription("This UID is already taken."); + return eb; + } + + if (secondaryUid != null) + { + var secondaryUser = await db.Auth.Include(u => u.PrimaryUser).Include(u => u.User) + .SingleOrDefaultAsync(u => u.UserUID == secondaryUid && u.PrimaryUserUID == lodestoneUser.User.UID).ConfigureAwait(false); + if (secondaryUser == null) + { + eb.WithTitle("No secondary UID found"); + eb.WithDescription($"Did not find a secondary UID {secondaryUid} attached to your primary UID {lodestoneUser.User.UID}"); + return eb; + } + + secondaryUser.User.Alias = newUid; + db.Update(secondaryUser); + + eb.WithTitle("Vanity UID set"); + eb.WithDescription($"Your Vanity UID for the secondary UID {secondaryUid} was set to **{newUid}**." + + Environment.NewLine + "For those changes to apply you will have to reconnect to Mare."); + } + else + { + var user = lodestoneUser.User; + user.Alias = newUid; + db.Update(user); + + eb.WithTitle("Vanity UID set"); + eb.WithDescription("Your Vanity UID was set to **" + newUid + "**." + + Environment.NewLine + "For those changes to apply you will have to reconnect to Mare."); + } + + await db.SaveChangesAsync(); + + _botServices.LastVanityChange[id] = DateTime.UtcNow; + return eb; + } + + private async Task HandleVanityGid(EmbedBuilder eb, ulong id, string oldGid, string newGid) + { + if (_botServices.LastVanityGidChange.TryGetValue(oldGid, out var lastChange)) + { + var dateTimeDiff = DateTime.UtcNow.Subtract(lastChange); + if (dateTimeDiff.TotalHours < 24) + { + eb.WithTitle(("Failed to set Vanity Syncshell Id")); + eb.WithDescription( + $"You can only change the Vanity Syncshell Id once every 24h. Your last change is {dateTimeDiff} ago."); + } + } + + Regex rgx = new(@"^[_\-a-zA-Z0-9]{5,20}$", RegexOptions.ECMAScript); + if (!rgx.Match(newGid).Success || newGid.Length < 5 || newGid.Length > 20) + { + eb.WithTitle("Failed to set Vanity Syncshell Id"); + eb.WithDescription("The Vanity Syncshell Id must be between 5 and 20 characters and only contain letters A-Z, numbers 0-9 as well as - and _."); + return eb; + } + + using var scope = _services.CreateScope(); + await using var db = scope.ServiceProvider.GetRequiredService(); + + var lodestoneUser = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.DiscordId == id).ConfigureAwait(false); + if (lodestoneUser == null) + { + eb.WithTitle("Failed to set Vanity Syncshell Id"); + eb.WithDescription("You do not have a registered account on this server."); + return eb; + } + + var group = await db.Groups.FirstOrDefaultAsync(g => g.GID == oldGid || g.Alias == oldGid).ConfigureAwait(false); + if (group == null) + { + eb.WithTitle("Failed to set Vanity Syncshell Id"); + eb.WithDescription("The provided Syncshell Id does not exist."); + return eb; + } + + if (lodestoneUser.User.UID != group.OwnerUID) + { + eb.WithTitle("Failed to set Vanity Syncshell Id"); + eb.WithDescription("You are not the owner of this Syncshell"); + return eb; + } + + var uidExists = await db.Groups.AnyAsync(u => u.GID == newGid || u.Alias == newGid).ConfigureAwait(false); + if (uidExists) + { + eb.WithTitle("Failed to set Vanity Syncshell Id"); + eb.WithDescription("This Syncshell Id is already taken."); + return eb; + } + + group.Alias = newGid; + db.Update(group); + await db.SaveChangesAsync(); + + _botServices.LastVanityGidChange[newGid] = DateTime.UtcNow; + _botServices.LastVanityGidChange[oldGid] = DateTime.UtcNow; + + eb.WithTitle("Vanity Syncshell Id set"); + eb.WithDescription("The Vanity Syncshell Id was set to **" + newGid + "**." + Environment.NewLine + "For those changes to apply you will have to reconnect to Mare."); + return eb; + } + + private async Task DeletePreviousUserAccount(ulong id) + { + using var scope = _services.CreateScope(); + using var db = scope.ServiceProvider.GetService(); + var discordAuthedUser = await db.LodeStoneAuth.Include(u => u.User).FirstOrDefaultAsync(u => u.DiscordId == id).ConfigureAwait(false); + if (discordAuthedUser != null) + { + if (discordAuthedUser.User != null) + { + var maxGroupsByUser = _mareClientConfigurationService.GetValueOrDefault(nameof(ServerConfiguration.MaxGroupUserCount), 3); + + await SharedDbFunctions.PurgeUser(_logger, discordAuthedUser.User, db, maxGroupsByUser); + } + else + { + db.Remove(discordAuthedUser); + } + + await db.SaveChangesAsync().ConfigureAwait(false); + } + } + + private async Task HandleVerifyRelinkAsync(SocketSlashCommand cmd, IServiceProvider serviceProvider) + { + var embedBuilder = new EmbedBuilder(); + + using var scope = serviceProvider.CreateScope(); + var req = new HttpClient(); + using var db = scope.ServiceProvider.GetService(); + + var lodestoneAuth = db.LodeStoneAuth.SingleOrDefault(u => u.DiscordId == cmd.User.Id); + if (lodestoneAuth != null && _botServices.DiscordRelinkLodestoneMapping.ContainsKey(cmd.User.Id)) + { + var randomServer = _botServices.LodestoneServers[random.Next(_botServices.LodestoneServers.Length)]; + var response = await req.GetAsync($"https://{randomServer}.finalfantasyxiv.com/lodestone/character/{_botServices.DiscordRelinkLodestoneMapping[cmd.User.Id]}").ConfigureAwait(false); + if (response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + if (content.Contains(lodestoneAuth.LodestoneAuthString)) + { + _botServices.DiscordRelinkLodestoneMapping.TryRemove(cmd.User.Id, out _); + + var existingLodestoneAuth = db.LodeStoneAuth.Include(u => u.User).SingleOrDefault(u => u.DiscordId != cmd.User.Id && u.HashedLodestoneId == lodestoneAuth.HashedLodestoneId); + + var previousAuth = await db.Auth.FirstOrDefaultAsync(u => u.UserUID == existingLodestoneAuth.User.UID && string.IsNullOrEmpty(u.PrimaryUserUID)); + if (previousAuth != null) + { + db.Auth.Remove(previousAuth); + } + + var computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString()); + var auth = new Auth() + { + HashedKey = StringUtils.Sha256String(computedHash), + User = existingLodestoneAuth.User, + }; + + lodestoneAuth.StartedAt = null; + lodestoneAuth.LodestoneAuthString = null; + lodestoneAuth.User = existingLodestoneAuth.User; + + db.LodeStoneAuth.Remove(existingLodestoneAuth); + + await db.Auth.AddAsync(auth).ConfigureAwait(false); + + _botServices.Logger.LogInformation("User relinked: {userUID}", lodestoneAuth.User.UID); + + embedBuilder.WithTitle("Relink successful"); + embedBuilder.WithDescription("This is your **new** private secret key. Do not share this private secret key with anyone. **If you lose it, it is irrevocably lost.**" + + Environment.NewLine + Environment.NewLine + + $"**{computedHash}**" + + Environment.NewLine + Environment.NewLine + + "Enter this key in Mare Synchronos and hit save to connect to the service."); + } + else + { + embedBuilder.WithTitle("Failed to verify your character"); + embedBuilder.WithDescription("Did not find requested authentication key on your profile. Make sure you have saved *twice*, then do **/relink_verify** again."); + lodestoneAuth.StartedAt = DateTime.UtcNow; + } + } + + await db.SaveChangesAsync().ConfigureAwait(false); + } + else + { + embedBuilder.WithTitle("Your auth has expired or something else went wrong"); + embedBuilder.WithDescription("Start again with **/relink**"); + _botServices.DiscordRelinkLodestoneMapping.TryRemove(cmd.User.Id, out _); + } + + var dataEmbed = embedBuilder.Build(); + + await cmd.FollowupAsync(embed: dataEmbed, ephemeral: true).ConfigureAwait(false); + } + + private async Task HandleVerifyAsync(SocketSlashCommand cmd, IServiceProvider serviceProvider) + { + var embedBuilder = new EmbedBuilder(); + + using var scope = serviceProvider.CreateScope(); + var req = new HttpClient(); + using var db = scope.ServiceProvider.GetService(); + + var lodestoneAuth = db.LodeStoneAuth.SingleOrDefault(u => u.DiscordId == cmd.User.Id); + if (lodestoneAuth != null && _botServices.DiscordLodestoneMapping.ContainsKey(cmd.User.Id)) + { + var randomServer = _botServices.LodestoneServers[random.Next(_botServices.LodestoneServers.Length)]; + var response = await req.GetAsync($"https://{randomServer}.finalfantasyxiv.com/lodestone/character/{_botServices.DiscordLodestoneMapping[cmd.User.Id]}").ConfigureAwait(false); + if (response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + if (content.Contains(lodestoneAuth.LodestoneAuthString)) + { + _botServices.DiscordLodestoneMapping.TryRemove(cmd.User.Id, out _); + + var user = new User(); + + var hasValidUid = false; + while (!hasValidUid) + { + var uid = StringUtils.GenerateRandomString(7); + if (db.Users.Any(u => u.UID == uid || u.Alias == uid)) continue; + user.UID = uid; + hasValidUid = true; + } + + // make the first registered user on the service to admin + if (!await db.Users.AnyAsync().ConfigureAwait(false)) + { + user.IsAdmin = true; + } + + user.LastLoggedIn = DateTime.UtcNow; + + var computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString()); + var auth = new Auth() + { + HashedKey = StringUtils.Sha256String(computedHash), + User = user, + }; + + await db.Users.AddAsync(user).ConfigureAwait(false); + await db.Auth.AddAsync(auth).ConfigureAwait(false); + + _botServices.Logger.LogInformation("User registered: {userUID}", user.UID); + + lodestoneAuth.StartedAt = null; + lodestoneAuth.User = user; + lodestoneAuth.LodestoneAuthString = null; + + embedBuilder.WithTitle("Registration successful"); + embedBuilder.WithDescription("This is your private secret key. Do not share this private secret key with anyone. **If you lose it, it is irrevocably lost.**" + + Environment.NewLine + Environment.NewLine + + $"**{computedHash}**" + + Environment.NewLine + Environment.NewLine + + "Enter this key in to the plugin when prompted and hit save to connect to the service." + + Environment.NewLine + + "You should connect as soon as possible to not get caught by the automatic cleanup process." + + Environment.NewLine + + "Have fun."); + } + else + { + embedBuilder.WithTitle("Failed to verify your character"); + embedBuilder.WithDescription("Did not find requested authentication key on your profile. Make sure you have saved *twice*, then do **/verify** again."); + lodestoneAuth.StartedAt = DateTime.UtcNow; + } + } + + await db.SaveChangesAsync().ConfigureAwait(false); + } + else + { + embedBuilder.WithTitle("Your auth has expired or something else went wrong"); + embedBuilder.WithDescription("Start again with **/register**"); + _botServices.DiscordLodestoneMapping.TryRemove(cmd.User.Id, out _); + } + + var dataEmbed = embedBuilder.Build(); + + await cmd.FollowupAsync(embed: dataEmbed, ephemeral: true).ConfigureAwait(false); + } +} diff --git a/MareSynchronosServer/MareSynchronosServices/DummyHub.cs b/MareSynchronosServer/MareSynchronosServices/DummyHub.cs new file mode 100644 index 0000000..afcb665 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/DummyHub.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.SignalR; + +// this is a very hacky way to attach this file server to the main mare hub signalr instance via redis +// signalr publishes the namespace and hubname into the redis backend so this needs to be equal to the original +// but I don't need to reimplement the hub completely as I only exclusively use it for internal connection calling +// from the queue service so I keep the namespace and name of the class the same so it can connect to the same channel +// if anyone finds a better way to do this let me know + +#pragma warning disable IDE0130 // Namespace does not match folder structure +#pragma warning disable MA0048 // File name must match type name +namespace MareSynchronosServer.Hubs; +public class MareHub : Hub +{ + public override Task OnConnectedAsync() + { + throw new NotSupportedException(); + } + + public override Task OnDisconnectedAsync(Exception exception) + { + throw new NotSupportedException(); + } +} +#pragma warning restore IDE0130 // Namespace does not match folder structure +#pragma warning restore MA0048 // File name must match type name \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServices/MareSynchronosServices.csproj b/MareSynchronosServer/MareSynchronosServices/MareSynchronosServices.csproj new file mode 100644 index 0000000..db6d3b3 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/MareSynchronosServices.csproj @@ -0,0 +1,38 @@ + + + + net8.0 + enable + + + + + + + + + + + + + + Never + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/MareSynchronosServer/MareSynchronosServices/Program.cs b/MareSynchronosServer/MareSynchronosServices/Program.cs new file mode 100644 index 0000000..4a3f930 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/Program.cs @@ -0,0 +1,43 @@ +using MareSynchronosServices; +using MareSynchronosShared.Data; +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils.Configuration; + +public class Program +{ + public static void Main(string[] args) + { + var hostBuilder = CreateHostBuilder(args); + var host = hostBuilder.Build(); + + using (var scope = host.Services.CreateScope()) + { + var services = scope.ServiceProvider; + using var dbContext = services.GetRequiredService(); + + var options = host.Services.GetService>(); + var optionsServer = host.Services.GetService>(); + var logger = host.Services.GetService>(); + logger.LogInformation("Loaded MareSynchronos Services Configuration (IsMain: {isMain})", options.IsMain); + logger.LogInformation(options.ToString()); + logger.LogInformation("Loaded MareSynchronos Server Configuration (IsMain: {isMain})", optionsServer.IsMain); + logger.LogInformation(optionsServer.ToString()); + } + + host.Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSystemd() + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseContentRoot(AppContext.BaseDirectory); + webBuilder.ConfigureLogging((ctx, builder) => + { + builder.AddConfiguration(ctx.Configuration.GetSection("Logging")); + builder.AddFile(o => o.RootPath = AppContext.BaseDirectory); + }); + webBuilder.UseStartup(); + }); +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServices/Properties/launchSettings.json b/MareSynchronosServer/MareSynchronosServices/Properties/launchSettings.json new file mode 100644 index 0000000..ea24f06 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "MareSynchronosServices": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5294;https://localhost:7294", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/MareSynchronosServer/MareSynchronosServices/Startup.cs b/MareSynchronosServer/MareSynchronosServices/Startup.cs new file mode 100644 index 0000000..e8b7437 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/Startup.cs @@ -0,0 +1,104 @@ +using MareSynchronosServices.Discord; +using MareSynchronosShared.Data; +using MareSynchronosShared.Metrics; +using Microsoft.EntityFrameworkCore; +using Prometheus; +using MareSynchronosShared.Utils; +using MareSynchronosShared.Services; +using StackExchange.Redis; +using MessagePack.Resolvers; +using MessagePack; +using MareSynchronosShared.Utils.Configuration; + +namespace MareSynchronosServices; + +public class Startup +{ + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + var config = app.ApplicationServices.GetRequiredService>(); + + var metricServer = new KestrelMetricServer(config.GetValueOrDefault(nameof(MareConfigurationBase.MetricsPort), 4982)); + metricServer.Start(); + + app.UseRouting(); + app.UseEndpoints(e => + { + e.MapHub("/dummyhub"); + }); + } + + public void ConfigureServices(IServiceCollection services) + { + var mareConfig = Configuration.GetSection("MareSynchronos"); + + services.AddDbContextPool(options => + { + options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder => + { + builder.MigrationsHistoryTable("_efmigrationshistory", "public"); + }).UseSnakeCaseNamingConvention(); + options.EnableThreadSafetyChecks(false); + }, Configuration.GetValue(nameof(MareConfigurationBase.DbContextPoolSize), 1024)); + + services.AddSingleton(m => new MareMetrics(m.GetService>(), new List { }, + new List { })); + + var redis = mareConfig.GetValue(nameof(ServerConfiguration.RedisConnectionString), string.Empty); + var options = ConfigurationOptions.Parse(redis); + options.ClientName = "Mare"; + options.ChannelPrefix = "UserData"; + ConnectionMultiplexer connectionMultiplexer = ConnectionMultiplexer.Connect(options); + services.AddSingleton(connectionMultiplexer); + + var signalRServiceBuilder = services.AddSignalR(hubOptions => + { + hubOptions.MaximumReceiveMessageSize = long.MaxValue; + hubOptions.EnableDetailedErrors = true; + hubOptions.MaximumParallelInvocationsPerClient = 10; + hubOptions.StreamBufferCapacity = 200; + }).AddMessagePackProtocol(opt => + { + var resolver = CompositeResolver.Create(StandardResolverAllowPrivate.Instance, + BuiltinResolver.Instance, + AttributeFormatterResolver.Instance, + // replace enum resolver + DynamicEnumAsStringResolver.Instance, + DynamicGenericResolver.Instance, + DynamicUnionResolver.Instance, + DynamicObjectResolver.Instance, + PrimitiveObjectResolver.Instance, + // final fallback(last priority) + StandardResolver.Instance); + + opt.SerializerOptions = MessagePackSerializerOptions.Standard + .WithCompression(MessagePackCompression.Lz4Block) + .WithResolver(resolver); + }); + + // configure redis for SignalR + var redisConnection = mareConfig.GetValue(nameof(MareConfigurationBase.RedisConnectionString), string.Empty); + signalRServiceBuilder.AddStackExchangeRedis(redisConnection, options => { }); + + services.Configure(Configuration.GetRequiredSection("MareSynchronos")); + services.Configure(Configuration.GetRequiredSection("MareSynchronos")); + services.Configure(Configuration.GetRequiredSection("MareSynchronos")); + services.AddSingleton(Configuration); + services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(); + services.AddSingleton, MareConfigurationServiceServer>(); + services.AddSingleton, MareConfigurationServiceClient>(); + services.AddSingleton, MareConfigurationServiceClient>(); + + services.AddHostedService(p => (MareConfigurationServiceClient)p.GetService>()); + services.AddHostedService(p => (MareConfigurationServiceClient)p.GetService>()); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServices/appsettings.Development.json b/MareSynchronosServer/MareSynchronosServices/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/MareSynchronosServer/MareSynchronosServices/appsettings.json b/MareSynchronosServer/MareSynchronosServices/appsettings.json new file mode 100644 index 0000000..902d25f --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/appsettings.json @@ -0,0 +1,29 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5432;Database=mare;Username=postgres" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Kestrel": { + "Endpoints": { + "Http": { + "Protocols": "Http2", + "Url": "http://+:5002" + } + } + }, + "MareSynchronos": { + "DbContextPoolSize": 1024, + "DiscordBotToken": "", + "DiscordChannelForMessages": "", + "PurgeUnusedAccounts": true, + "PurgeUnusedAccountsPeriodInDays": 14, + "FailedAuthForTempBan": 5, + "TempBanDurationInMinutes": 30, + }, + "AllowedHosts": "*" +} diff --git a/MareSynchronosServer/MareSynchronosShared/Data/MareDbContext.cs b/MareSynchronosServer/MareSynchronosShared/Data/MareDbContext.cs new file mode 100644 index 0000000..0e78bb5 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Data/MareDbContext.cs @@ -0,0 +1,131 @@ +using MareSynchronosShared.Models; +using Microsoft.EntityFrameworkCore; + +namespace MareSynchronosShared.Data; + +public class MareDbContext : DbContext +{ +#if DEBUG + public MareDbContext() { } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (optionsBuilder.IsConfigured) + { + base.OnConfiguring(optionsBuilder); + return; + } + + optionsBuilder.UseNpgsql("Host=localhost;Port=5432;Database=mare;Username=postgres", builder => + { + builder.MigrationsHistoryTable("_efmigrationshistory", "public"); + builder.MigrationsAssembly("MareSynchronosShared"); + }).UseSnakeCaseNamingConvention(); + optionsBuilder.EnableThreadSafetyChecks(false); + + base.OnConfiguring(optionsBuilder); + } +#endif + + public MareDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Auth { get; set; } + public DbSet BannedRegistrations { get; set; } + public DbSet BannedUsers { get; set; } + public DbSet ClientPairs { get; set; } + public DbSet Files { get; set; } + public DbSet ForbiddenUploadEntries { get; set; } + public DbSet GroupBans { get; set; } + public DbSet GroupPairs { get; set; } + public DbSet Groups { get; set; } + public DbSet GroupTempInvites { get; set; } + public DbSet LodeStoneAuth { get; set; } + public DbSet UserProfileData { get; set; } + public DbSet UserProfileReports { get; set; } + public DbSet Users { get; set; } + public DbSet CharaData { get; set; } + public DbSet CharaDataFiles { get; set; } + public DbSet CharaDataFileSwaps { get; set; } + public DbSet CharaDataOriginalFiles { get; set; } + public DbSet CharaDataPoses { get; set; } + public DbSet CharaDataAllowances { get; set; } + + protected override void OnModelCreating(ModelBuilder mb) + { + mb.Entity().ToTable("auth"); + mb.Entity().ToTable("users"); + mb.Entity().ToTable("file_caches"); + mb.Entity().HasIndex(c => c.UploaderUID); + mb.Entity().ToTable("client_pairs"); + mb.Entity().HasKey(u => new { u.UserUID, u.OtherUserUID }); + mb.Entity().HasIndex(c => c.UserUID); + mb.Entity().HasIndex(c => c.OtherUserUID); + mb.Entity().ToTable("forbidden_upload_entries"); + mb.Entity().ToTable("banned_users"); + mb.Entity().ToTable("lodestone_auth"); + mb.Entity().ToTable("banned_registrations"); + mb.Entity().ToTable("groups"); + mb.Entity().HasIndex(c => c.OwnerUID); + mb.Entity().ToTable("group_pairs"); + mb.Entity().HasKey(u => new { u.GroupGID, u.GroupUserUID }); + mb.Entity().HasIndex(c => c.GroupUserUID); + mb.Entity().HasIndex(c => c.GroupGID); + mb.Entity().ToTable("group_bans"); + mb.Entity().HasKey(u => new { u.GroupGID, u.BannedUserUID }); + mb.Entity().HasIndex(c => c.BannedUserUID); + mb.Entity().HasIndex(c => c.GroupGID); + mb.Entity().ToTable("group_temp_invites"); + mb.Entity().HasKey(u => new { u.GroupGID, u.Invite }); + mb.Entity().HasIndex(c => c.GroupGID); + mb.Entity().HasIndex(c => c.Invite); + mb.Entity().ToTable("user_profile_data"); + mb.Entity().HasKey(c => c.UserUID); + mb.Entity().ToTable("user_profile_data_reports"); + mb.Entity().ToTable("chara_data"); + mb.Entity() + .HasMany(p => p.Poses) + .WithOne(c => c.Parent) + .HasForeignKey(c => new { c.ParentId, c.ParentUploaderUID }); + mb.Entity() + .HasMany(p => p.Files) + .WithOne(c => c.Parent) + .HasForeignKey(c => new { c.ParentId, c.ParentUploaderUID }); + mb.Entity() + .HasMany(p => p.OriginalFiles) + .WithOne(p => p.Parent) + .HasForeignKey(p => new { p.ParentId, p.ParentUploaderUID }); + mb.Entity() + .HasMany(p => p.AllowedIndividiuals) + .WithOne(p => p.Parent) + .HasForeignKey(p => new { p.ParentId, p.ParentUploaderUID }); + mb.Entity() + .HasMany(p => p.FileSwaps) + .WithOne(p => p.Parent) + .HasForeignKey(p => new { p.ParentId, p.ParentUploaderUID }); + mb.Entity().HasKey(p => new { p.Id, p.UploaderUID }); + mb.Entity().HasIndex(p => p.UploaderUID); + mb.Entity().HasIndex(p => p.Id); + mb.Entity().ToTable("chara_data_files"); + mb.Entity().HasKey(c => new { c.ParentId, c.ParentUploaderUID, c.GamePath }); + mb.Entity().HasIndex(c => c.ParentId); + mb.Entity().HasOne(f => f.FileCache).WithMany().HasForeignKey(f => f.FileCacheHash).OnDelete(DeleteBehavior.Cascade); + mb.Entity().ToTable("chara_data_file_swaps"); + mb.Entity().HasKey(c => new { c.ParentId, c.ParentUploaderUID, c.GamePath }); + mb.Entity().HasIndex(c => c.ParentId); + mb.Entity().ToTable("chara_data_poses"); + mb.Entity().Property(p => p.Id).ValueGeneratedOnAdd(); + mb.Entity().HasKey(c => new { c.ParentId, c.ParentUploaderUID, c.Id }); + mb.Entity().HasIndex(c => c.ParentId); + mb.Entity().ToTable("chara_data_orig_files"); + mb.Entity().HasKey(c => new { c.ParentId, c.ParentUploaderUID, c.GamePath }); + mb.Entity().HasIndex(c => c.ParentId); + mb.Entity().ToTable("chara_data_allowance"); + mb.Entity().HasKey(c => new { c.ParentId, c.ParentUploaderUID, c.Id }); + mb.Entity().Property(p => p.Id).ValueGeneratedOnAdd(); + mb.Entity().HasIndex(c => c.ParentId); + mb.Entity().HasOne(u => u.AllowedGroup).WithMany().HasForeignKey(u => u.AllowedGroupGID).OnDelete(DeleteBehavior.Cascade); + mb.Entity().HasOne(u => u.AllowedUser).WithMany().HasForeignKey(u => u.AllowedUserUID).OnDelete(DeleteBehavior.Cascade); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Extensions.cs b/MareSynchronosServer/MareSynchronosShared/Extensions.cs new file mode 100644 index 0000000..abcac4f --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Extensions.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Http; + +namespace MareSynchronosShared; + +public static class Extensions +{ + private static long _noIpCntr = 0; + public static string GetIpAddress(this IHttpContextAccessor accessor) + { + try + { + if (!string.IsNullOrEmpty(accessor.HttpContext.Request.Headers["CF-CONNECTING-IP"])) + return accessor.HttpContext.Request.Headers["CF-CONNECTING-IP"]; + + if (!string.IsNullOrEmpty(accessor.HttpContext.Request.Headers["X-Forwarded-For"])) + { + return accessor.HttpContext.Request.Headers["X-Forwarded-For"]; + } + + var ipAddress = accessor.HttpContext.GetServerVariable("HTTP_X_FORWARDED_FOR"); + + if (!string.IsNullOrWhiteSpace(ipAddress)) + { + var addresses = ipAddress.Split(',', StringSplitOptions.RemoveEmptyEntries); + var lastEntry = addresses.LastOrDefault(); + if (lastEntry != null) + { + return lastEntry; + } + } + + return accessor.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "NoIp"; + } + catch + { + return "NoIp" + _noIpCntr++; + } + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/MareSynchronosShared.csproj b/MareSynchronosServer/MareSynchronosShared/MareSynchronosShared.csproj new file mode 100644 index 0000000..f1e1092 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/MareSynchronosShared.csproj @@ -0,0 +1,59 @@ + + + + net8.0 + enable + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + diff --git a/MareSynchronosServer/MareSynchronosShared/Metrics/MareMetrics.cs b/MareSynchronosServer/MareSynchronosShared/Metrics/MareMetrics.cs new file mode 100644 index 0000000..624e907 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Metrics/MareMetrics.cs @@ -0,0 +1,84 @@ +using Microsoft.Extensions.Logging; +using Prometheus; + +namespace MareSynchronosShared.Metrics; + +public class MareMetrics +{ + public MareMetrics(ILogger logger, List countersToServe, List gaugesToServe) + { + logger.LogInformation("Initializing MareMetrics"); + foreach (var counter in countersToServe) + { + logger.LogInformation($"Creating Metric for Counter {counter}"); + counters.Add(counter, Prometheus.Metrics.CreateCounter(counter, counter)); + } + + foreach (var gauge in gaugesToServe) + { + logger.LogInformation($"Creating Metric for Counter {gauge}"); + if (!string.Equals(gauge, MetricsAPI.GaugeConnections, StringComparison.OrdinalIgnoreCase)) + gauges.Add(gauge, Prometheus.Metrics.CreateGauge(gauge, gauge)); + else + gauges.Add(gauge, Prometheus.Metrics.CreateGauge(gauge, gauge, new[] { "continent" })); + } + } + + private readonly Dictionary counters = new(StringComparer.Ordinal); + + private readonly Dictionary gauges = new(StringComparer.Ordinal); + + public void IncGaugeWithLabels(string gaugeName, double value = 1.0, params string[] labels) + { + if (gauges.TryGetValue(gaugeName, out Gauge gauge)) + { + lock (gauge) + gauge.WithLabels(labels).Inc(value); + } + } + + public void DecGaugeWithLabels(string gaugeName, double value = 1.0, params string[] labels) + { + if (gauges.TryGetValue(gaugeName, out Gauge gauge)) + { + lock (gauge) + gauge.WithLabels(labels).Dec(value); + } + } + + public void SetGaugeTo(string gaugeName, double value) + { + if (gauges.TryGetValue(gaugeName, out Gauge gauge)) + { + lock (gauge) + gauge.Set(value); + } + } + + public void IncGauge(string gaugeName, double value = 1.0) + { + if (gauges.TryGetValue(gaugeName, out Gauge gauge)) + { + lock (gauge) + gauge.Inc(value); + } + } + + public void DecGauge(string gaugeName, double value = 1.0) + { + if (gauges.TryGetValue(gaugeName, out Gauge gauge)) + { + lock (gauge) + gauge.Dec(value); + } + } + + public void IncCounter(string counterName, double value = 1.0) + { + if (counters.TryGetValue(counterName, out Counter counter)) + { + lock (counter) + counter.Inc(value); + } + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Metrics/MetricsAPI.cs b/MareSynchronosServer/MareSynchronosShared/Metrics/MetricsAPI.cs new file mode 100644 index 0000000..d3aee90 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Metrics/MetricsAPI.cs @@ -0,0 +1,46 @@ +namespace MareSynchronosShared.Metrics; + +public class MetricsAPI +{ + public const string CounterInitializedConnections = "mare_initialized_connections"; + public const string GaugeConnections = "mare_connections"; + public const string GaugeAuthorizedConnections = "mare_authorized_connections"; + public const string GaugeAvailableWorkerThreads = "mare_available_threadpool"; + public const string GaugeAvailableIOWorkerThreads = "mare_available_threadpool_io"; + public const string GaugeUsersRegistered = "mare_users_registered"; + public const string CounterUsersRegisteredDeleted = "mare_users_registered_deleted"; + public const string GaugePairs = "mare_pairs"; + public const string GaugePairsPaused = "mare_pairs_paused"; + public const string GaugeFilesTotal = "mare_files"; + public const string GaugeFilesTotalColdStorage = "mare_files_cold"; + public const string GaugeFilesTotalSize = "mare_files_size"; + public const string GaugeFilesTotalSizeColdStorage = "mare_files_size_cold"; + public const string GaugeFilesDownloadingFromCache = "mare_files_downloading_from_cache"; + public const string GaugeFilesTasksWaitingForDownloadFromCache = "mare_files_waiting_for_dl"; + public const string CounterUserPushData = "mare_user_push"; + public const string CounterUserPushDataTo = "mare_user_push_to"; + public const string CounterAuthenticationRequests = "mare_auth_requests"; + public const string CounterAuthenticationCacheHits = "mare_auth_requests_cachehit"; + public const string CounterAuthenticationFailures = "mare_auth_requests_fail"; + public const string CounterAuthenticationSuccesses = "mare_auth_requests_success"; + public const string GaugeGroups = "mare_groups"; + public const string GaugeGroupPairs = "mare_groups_pairs"; + public const string GaugeGroupPairsPaused = "mare_groups_pairs_paused"; + public const string GaugeFilesUniquePastHour = "mare_files_unique_past_hour"; + public const string GaugeFilesUniquePastHourSize = "mare_files_unique_past_hour_size"; + public const string GaugeFilesUniquePastDay = "mare_files_unique_past_day"; + public const string GaugeFilesUniquePastDaySize = "mare_files_unique_past_day_size"; + public const string GaugeCurrentDownloads = "mare_current_downloads"; + public const string GaugeQueueFree = "mare_download_queue_free"; + public const string GaugeQueueActive = "mare_download_queue_active"; + public const string GaugeQueueInactive = "mare_download_queue_inactive"; + public const string GaugeDownloadQueue = "mare_download_queue"; + public const string GaugeDownloadQueueCancelled = "mare_download_queue_cancelled"; + public const string GaugeDownloadPriorityQueue = "mare_download_priority_queue"; + public const string GaugeDownloadPriorityQueueCancelled = "mare_download_priority_queue_cancelled"; + public const string CounterFileRequests = "mare_files_requests"; + public const string CounterFileRequestSize = "mare_files_request_size"; + public const string CounterAccountsCreated = "mare_accounts_created"; + public const string GaugeGposeLobbies = "mare_gpose_lobbies"; + public const string GaugeGposeLobbyUsers = "mare_gpose_lobby_users"; +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220731210149_InitialCreate.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220731210149_InitialCreate.Designer.cs new file mode 100644 index 0000000..31ea23a --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220731210149_InitialCreate.Designer.cs @@ -0,0 +1,241 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20220731210149_InitialCreate")] + partial class InitialCreate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosServer.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("Auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasColumnType("text") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("BannedUsers", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("ClientPairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("FileCaches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasColumnType("text") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("ForbiddenUploadEntries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("CharacterIdentification") + .HasColumnType("text") + .HasColumnName("character_identification"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.HasIndex("CharacterIdentification") + .HasDatabaseName("ix_users_character_identification"); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.Auth", b => + { + b.HasOne("MareSynchronosServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.ClientPair", b => + { + b.HasOne("MareSynchronosServer.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_uid"); + + b.HasOne("MareSynchronosServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_uid"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.FileCache", b => + { + b.HasOne("MareSynchronosServer.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220731210149_InitialCreate.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220731210149_InitialCreate.cs new file mode 100644 index 0000000..0fec13f --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220731210149_InitialCreate.cs @@ -0,0 +1,163 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + public partial class InitialCreate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "BannedUsers", + columns: table => new + { + character_identification = table.Column(type: "text", nullable: false), + reason = table.Column(type: "text", nullable: true), + timestamp = table.Column(type: "bytea", rowVersion: true, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_banned_users", x => x.character_identification); + }); + + migrationBuilder.CreateTable( + name: "ForbiddenUploadEntries", + columns: table => new + { + hash = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), + forbidden_by = table.Column(type: "text", nullable: true), + timestamp = table.Column(type: "bytea", rowVersion: true, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_forbidden_upload_entries", x => x.hash); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + uid = table.Column(type: "character varying(10)", maxLength: 10, nullable: false), + character_identification = table.Column(type: "text", nullable: true), + timestamp = table.Column(type: "bytea", rowVersion: true, nullable: true), + is_moderator = table.Column(type: "boolean", nullable: false), + is_admin = table.Column(type: "boolean", nullable: false), + last_logged_in = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_users", x => x.uid); + }); + + migrationBuilder.CreateTable( + name: "Auth", + columns: table => new + { + hashed_key = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + user_uid = table.Column(type: "character varying(10)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_auth", x => x.hashed_key); + table.ForeignKey( + name: "fk_auth_users_user_uid", + column: x => x.user_uid, + principalTable: "Users", + principalColumn: "uid"); + }); + + migrationBuilder.CreateTable( + name: "ClientPairs", + columns: table => new + { + user_uid = table.Column(type: "character varying(10)", maxLength: 10, nullable: false), + other_user_uid = table.Column(type: "character varying(10)", maxLength: 10, nullable: false), + is_paused = table.Column(type: "boolean", nullable: false), + allow_receiving_messages = table.Column(type: "boolean", nullable: false), + timestamp = table.Column(type: "bytea", rowVersion: true, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_client_pairs", x => new { x.user_uid, x.other_user_uid }); + table.ForeignKey( + name: "fk_client_pairs_users_other_user_uid", + column: x => x.other_user_uid, + principalTable: "Users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_client_pairs_users_user_uid", + column: x => x.user_uid, + principalTable: "Users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "FileCaches", + columns: table => new + { + hash = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), + uploader_uid = table.Column(type: "character varying(10)", nullable: true), + uploaded = table.Column(type: "boolean", nullable: false), + timestamp = table.Column(type: "bytea", rowVersion: true, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_file_caches", x => x.hash); + table.ForeignKey( + name: "fk_file_caches_users_uploader_uid", + column: x => x.uploader_uid, + principalTable: "Users", + principalColumn: "uid"); + }); + + migrationBuilder.CreateIndex( + name: "ix_auth_user_uid", + table: "Auth", + column: "user_uid"); + + migrationBuilder.CreateIndex( + name: "ix_client_pairs_other_user_uid", + table: "ClientPairs", + column: "other_user_uid"); + + migrationBuilder.CreateIndex( + name: "ix_client_pairs_user_uid", + table: "ClientPairs", + column: "user_uid"); + + migrationBuilder.CreateIndex( + name: "ix_file_caches_uploader_uid", + table: "FileCaches", + column: "uploader_uid"); + + migrationBuilder.CreateIndex( + name: "ix_users_character_identification", + table: "Users", + column: "character_identification"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Auth"); + + migrationBuilder.DropTable( + name: "BannedUsers"); + + migrationBuilder.DropTable( + name: "ClientPairs"); + + migrationBuilder.DropTable( + name: "FileCaches"); + + migrationBuilder.DropTable( + name: "ForbiddenUploadEntries"); + + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220731211419_RenameLowerSnakeCase.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220731211419_RenameLowerSnakeCase.Designer.cs new file mode 100644 index 0000000..59c7501 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220731211419_RenameLowerSnakeCase.Designer.cs @@ -0,0 +1,241 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20220731211419_RenameLowerSnakeCase")] + partial class RenameLowerSnakeCase + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosServer.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasColumnType("text") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasColumnType("text") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("CharacterIdentification") + .HasColumnType("text") + .HasColumnName("character_identification"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.HasIndex("CharacterIdentification") + .HasDatabaseName("ix_users_character_identification"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.Auth", b => + { + b.HasOne("MareSynchronosServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.ClientPair", b => + { + b.HasOne("MareSynchronosServer.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("MareSynchronosServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id2"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.FileCache", b => + { + b.HasOne("MareSynchronosServer.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220731211419_RenameLowerSnakeCase.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220731211419_RenameLowerSnakeCase.cs new file mode 100644 index 0000000..aaaf083 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220731211419_RenameLowerSnakeCase.cs @@ -0,0 +1,133 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + public partial class RenameLowerSnakeCase : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_auth_users_user_uid", + table: "Auth"); + + migrationBuilder.DropForeignKey( + name: "fk_client_pairs_users_other_user_uid", + table: "ClientPairs"); + + migrationBuilder.DropForeignKey( + name: "fk_client_pairs_users_user_uid", + table: "ClientPairs"); + + migrationBuilder.RenameTable( + name: "Users", + newName: "users"); + + migrationBuilder.RenameTable( + name: "Auth", + newName: "auth"); + + migrationBuilder.RenameTable( + name: "ForbiddenUploadEntries", + newName: "forbidden_upload_entries"); + + migrationBuilder.RenameTable( + name: "FileCaches", + newName: "file_caches"); + + migrationBuilder.RenameTable( + name: "ClientPairs", + newName: "client_pairs"); + + migrationBuilder.RenameTable( + name: "BannedUsers", + newName: "banned_users"); + + migrationBuilder.AddForeignKey( + name: "fk_auth_users_user_temp_id", + table: "auth", + column: "user_uid", + principalTable: "users", + principalColumn: "uid"); + + migrationBuilder.AddForeignKey( + name: "fk_client_pairs_users_other_user_temp_id1", + table: "client_pairs", + column: "other_user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_client_pairs_users_user_temp_id2", + table: "client_pairs", + column: "user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_auth_users_user_temp_id", + table: "auth"); + + migrationBuilder.DropForeignKey( + name: "fk_client_pairs_users_other_user_temp_id1", + table: "client_pairs"); + + migrationBuilder.DropForeignKey( + name: "fk_client_pairs_users_user_temp_id2", + table: "client_pairs"); + + migrationBuilder.RenameTable( + name: "users", + newName: "Users"); + + migrationBuilder.RenameTable( + name: "auth", + newName: "Auth"); + + migrationBuilder.RenameTable( + name: "forbidden_upload_entries", + newName: "ForbiddenUploadEntries"); + + migrationBuilder.RenameTable( + name: "file_caches", + newName: "FileCaches"); + + migrationBuilder.RenameTable( + name: "client_pairs", + newName: "ClientPairs"); + + migrationBuilder.RenameTable( + name: "banned_users", + newName: "BannedUsers"); + + migrationBuilder.AddForeignKey( + name: "fk_auth_users_user_uid", + table: "Auth", + column: "user_uid", + principalTable: "Users", + principalColumn: "uid"); + + migrationBuilder.AddForeignKey( + name: "fk_client_pairs_users_other_user_uid", + table: "ClientPairs", + column: "other_user_uid", + principalTable: "Users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_client_pairs_users_user_uid", + table: "ClientPairs", + column: "user_uid", + principalTable: "Users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220801121419_AddLodestoneAuth.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220801121419_AddLodestoneAuth.Designer.cs new file mode 100644 index 0000000..5bab84a --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220801121419_AddLodestoneAuth.Designer.cs @@ -0,0 +1,283 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20220801121419_AddLodestoneAuth")] + partial class AddLodestoneAuth + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosServer.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasColumnType("text") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasColumnType("text") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasColumnType("text") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasColumnType("text") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("CharacterIdentification") + .HasColumnType("text") + .HasColumnName("character_identification"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.HasIndex("CharacterIdentification") + .HasDatabaseName("ix_users_character_identification"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.Auth", b => + { + b.HasOne("MareSynchronosServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.ClientPair", b => + { + b.HasOne("MareSynchronosServer.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("MareSynchronosServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id2"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.FileCache", b => + { + b.HasOne("MareSynchronosServer.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220801121419_AddLodestoneAuth.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220801121419_AddLodestoneAuth.cs new file mode 100644 index 0000000..13bc0ac --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220801121419_AddLodestoneAuth.cs @@ -0,0 +1,43 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + public partial class AddLodestoneAuth : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "lodestone_auth", + columns: table => new + { + discord_id = table.Column(type: "numeric(20,0)", nullable: false), + hashed_lodestone_id = table.Column(type: "text", nullable: true), + lodestone_auth_string = table.Column(type: "text", nullable: true), + user_uid = table.Column(type: "character varying(10)", nullable: true), + started_at = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_lodestone_auth", x => x.discord_id); + table.ForeignKey( + name: "fk_lodestone_auth_users_user_uid", + column: x => x.user_uid, + principalTable: "users", + principalColumn: "uid"); + }); + + migrationBuilder.CreateIndex( + name: "ix_lodestone_auth_user_uid", + table: "lodestone_auth", + column: "user_uid"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "lodestone_auth"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.Designer.cs new file mode 100644 index 0000000..3b29f0d --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.Designer.cs @@ -0,0 +1,283 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20220801122103_AddNullableLodestoneAuthProperties")] + partial class AddNullableLodestoneAuthProperties + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosServer.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasColumnType("text") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasColumnType("text") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasColumnType("text") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasColumnType("text") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("CharacterIdentification") + .HasColumnType("text") + .HasColumnName("character_identification"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.HasIndex("CharacterIdentification") + .HasDatabaseName("ix_users_character_identification"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.Auth", b => + { + b.HasOne("MareSynchronosServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.ClientPair", b => + { + b.HasOne("MareSynchronosServer.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("MareSynchronosServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id2"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.FileCache", b => + { + b.HasOne("MareSynchronosServer.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.cs new file mode 100644 index 0000000..1692083 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + public partial class AddNullableLodestoneAuthProperties : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "started_at", + table: "lodestone_auth", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "started_at", + table: "lodestone_auth", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220806103053_AddBannedRegistrations.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220806103053_AddBannedRegistrations.Designer.cs new file mode 100644 index 0000000..82a1dcf --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220806103053_AddBannedRegistrations.Designer.cs @@ -0,0 +1,295 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20220806103053_AddBannedRegistrations")] + partial class AddBannedRegistrations + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosServer.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasColumnType("text") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasColumnType("text") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasColumnType("text") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasColumnType("text") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasColumnType("text") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("CharacterIdentification") + .HasColumnType("text") + .HasColumnName("character_identification"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.HasIndex("CharacterIdentification") + .HasDatabaseName("ix_users_character_identification"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.Auth", b => + { + b.HasOne("MareSynchronosServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.ClientPair", b => + { + b.HasOne("MareSynchronosServer.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("MareSynchronosServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id2"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.FileCache", b => + { + b.HasOne("MareSynchronosServer.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220806103053_AddBannedRegistrations.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220806103053_AddBannedRegistrations.cs new file mode 100644 index 0000000..954eb2d --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220806103053_AddBannedRegistrations.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + public partial class AddBannedRegistrations : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "banned_registrations", + columns: table => new + { + discord_id_or_lodestone_auth = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_banned_registrations", x => x.discord_id_or_lodestone_auth); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "banned_registrations"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220816170426_SetMaxLimitForStrings.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220816170426_SetMaxLimitForStrings.Designer.cs new file mode 100644 index 0000000..66a6821 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220816170426_SetMaxLimitForStrings.Designer.cs @@ -0,0 +1,302 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20220816170426_SetMaxLimitForStrings")] + partial class SetMaxLimitForStrings + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosServer.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.HasIndex("CharacterIdentification") + .HasDatabaseName("ix_users_character_identification"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.Auth", b => + { + b.HasOne("MareSynchronosServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.ClientPair", b => + { + b.HasOne("MareSynchronosServer.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("MareSynchronosServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id2"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.FileCache", b => + { + b.HasOne("MareSynchronosServer.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220816170426_SetMaxLimitForStrings.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220816170426_SetMaxLimitForStrings.cs new file mode 100644 index 0000000..a6cbde0 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220816170426_SetMaxLimitForStrings.cs @@ -0,0 +1,131 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + public partial class SetMaxLimitForStrings : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "character_identification", + table: "users", + type: "character varying(100)", + maxLength: 100, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "lodestone_auth_string", + table: "lodestone_auth", + type: "character varying(100)", + maxLength: 100, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "hashed_lodestone_id", + table: "lodestone_auth", + type: "character varying(100)", + maxLength: 100, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "forbidden_by", + table: "forbidden_upload_entries", + type: "character varying(100)", + maxLength: 100, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "character_identification", + table: "banned_users", + type: "character varying(100)", + maxLength: 100, + nullable: false, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "discord_id_or_lodestone_auth", + table: "banned_registrations", + type: "character varying(100)", + maxLength: 100, + nullable: false, + oldClrType: typeof(string), + oldType: "text"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "character_identification", + table: "users", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(100)", + oldMaxLength: 100, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "lodestone_auth_string", + table: "lodestone_auth", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(100)", + oldMaxLength: 100, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "hashed_lodestone_id", + table: "lodestone_auth", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(100)", + oldMaxLength: 100, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "forbidden_by", + table: "forbidden_upload_entries", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(100)", + oldMaxLength: 100, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "character_identification", + table: "banned_users", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(100)", + oldMaxLength: 100); + + migrationBuilder.AlterColumn( + name: "discord_id_or_lodestone_auth", + table: "banned_registrations", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(100)", + oldMaxLength: 100); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220824225157_AddAlias.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220824225157_AddAlias.Designer.cs new file mode 100644 index 0000000..256f0ef --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220824225157_AddAlias.Designer.cs @@ -0,0 +1,307 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20220824225157_AddAlias")] + partial class AddAlias + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("alias"); + + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.HasIndex("CharacterIdentification") + .HasDatabaseName("ix_users_character_identification"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id2"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220824225157_AddAlias.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220824225157_AddAlias.cs new file mode 100644 index 0000000..f18b67f --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220824225157_AddAlias.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + public partial class AddAlias : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "alias", + table: "users", + type: "character varying(10)", + maxLength: 10, + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "alias", + table: "users"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220917115233_Groups.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220917115233_Groups.Designer.cs new file mode 100644 index 0000000..1a45495 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220917115233_Groups.Designer.cs @@ -0,0 +1,389 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20220917115233_Groups")] + partial class Groups + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(14) + .HasColumnType("character varying(14)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(14)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id2"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id5"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id4"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220917115233_Groups.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220917115233_Groups.cs new file mode 100644 index 0000000..39c1604 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220917115233_Groups.cs @@ -0,0 +1,123 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + public partial class Groups : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_users_character_identification", + table: "users"); + + migrationBuilder.DropColumn( + name: "character_identification", + table: "users"); + + migrationBuilder.AlterColumn( + name: "alias", + table: "users", + type: "character varying(10)", + maxLength: 10, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(100)", + oldMaxLength: 100, + oldNullable: true); + + migrationBuilder.CreateTable( + name: "groups", + columns: table => new + { + gid = table.Column(type: "character varying(14)", maxLength: 14, nullable: false), + owner_uid = table.Column(type: "character varying(10)", nullable: true), + alias = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + invites_enabled = table.Column(type: "boolean", nullable: false), + hashed_password = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_groups", x => x.gid); + table.ForeignKey( + name: "fk_groups_users_owner_temp_id5", + column: x => x.owner_uid, + principalTable: "users", + principalColumn: "uid"); + }); + + migrationBuilder.CreateTable( + name: "group_pairs", + columns: table => new + { + group_gid = table.Column(type: "character varying(14)", nullable: false), + group_user_uid = table.Column(type: "character varying(10)", nullable: false), + is_paused = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_group_pairs", x => new { x.group_gid, x.group_user_uid }); + table.ForeignKey( + name: "fk_group_pairs_groups_group_temp_id", + column: x => x.group_gid, + principalTable: "groups", + principalColumn: "gid", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_group_pairs_users_group_user_temp_id4", + column: x => x.group_user_uid, + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_group_pairs_group_gid", + table: "group_pairs", + column: "group_gid"); + + migrationBuilder.CreateIndex( + name: "ix_group_pairs_group_user_uid", + table: "group_pairs", + column: "group_user_uid"); + + migrationBuilder.CreateIndex( + name: "ix_groups_owner_uid", + table: "groups", + column: "owner_uid"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "group_pairs"); + + migrationBuilder.DropTable( + name: "groups"); + + migrationBuilder.AlterColumn( + name: "alias", + table: "users", + type: "character varying(100)", + maxLength: 100, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(10)", + oldMaxLength: 10, + oldNullable: true); + + migrationBuilder.AddColumn( + name: "character_identification", + table: "users", + type: "character varying(100)", + maxLength: 100, + nullable: true); + + migrationBuilder.CreateIndex( + name: "ix_users_character_identification", + table: "users", + column: "character_identification"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220929150304_ChangeGidLength.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220929150304_ChangeGidLength.Designer.cs new file mode 100644 index 0000000..7699c58 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220929150304_ChangeGidLength.Designer.cs @@ -0,0 +1,389 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20220929150304_ChangeGidLength")] + partial class ChangeGidLength + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id2"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id5"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id4"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220929150304_ChangeGidLength.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220929150304_ChangeGidLength.cs new file mode 100644 index 0000000..ec9dcc1 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220929150304_ChangeGidLength.cs @@ -0,0 +1,51 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + public partial class ChangeGidLength : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "gid", + table: "groups", + type: "character varying(20)", + maxLength: 20, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(14)", + oldMaxLength: 14); + + migrationBuilder.AlterColumn( + name: "group_gid", + table: "group_pairs", + type: "character varying(20)", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(14)"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "gid", + table: "groups", + type: "character varying(14)", + maxLength: 14, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(20)", + oldMaxLength: 20); + + migrationBuilder.AlterColumn( + name: "group_gid", + table: "group_pairs", + type: "character varying(14)", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(20)"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20221002105428_IsPinned.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20221002105428_IsPinned.Designer.cs new file mode 100644 index 0000000..4285fe9 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20221002105428_IsPinned.Designer.cs @@ -0,0 +1,393 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20221002105428_IsPinned")] + partial class IsPinned + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id2"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id5"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id4"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20221002105428_IsPinned.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20221002105428_IsPinned.cs new file mode 100644 index 0000000..3ee9c31 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20221002105428_IsPinned.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + public partial class IsPinned : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "is_pinned", + table: "group_pairs", + type: "boolean", + nullable: false, + defaultValue: false); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "is_pinned", + table: "group_pairs"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20221004125939_AdjustAliasLength.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20221004125939_AdjustAliasLength.Designer.cs new file mode 100644 index 0000000..0b5c721 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20221004125939_AdjustAliasLength.Designer.cs @@ -0,0 +1,393 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20221004125939_AdjustAliasLength")] + partial class AdjustAliasLength + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id2"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id5"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id4"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20221004125939_AdjustAliasLength.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20221004125939_AdjustAliasLength.cs new file mode 100644 index 0000000..cb623d2 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20221004125939_AdjustAliasLength.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + public partial class AdjustAliasLength : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "alias", + table: "users", + type: "character varying(15)", + maxLength: 15, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(10)", + oldMaxLength: 10, + oldNullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "alias", + table: "users", + type: "character varying(10)", + maxLength: 10, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(15)", + oldMaxLength: 15, + oldNullable: true); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20221006115929_GroupModerator.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20221006115929_GroupModerator.Designer.cs new file mode 100644 index 0000000..56ecd28 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20221006115929_GroupModerator.Designer.cs @@ -0,0 +1,397 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20221006115929_GroupModerator")] + partial class GroupModerator + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id2"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id5"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id4"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20221006115929_GroupModerator.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20221006115929_GroupModerator.cs new file mode 100644 index 0000000..e0b45c1 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20221006115929_GroupModerator.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + public partial class GroupModerator : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "is_moderator", + table: "group_pairs", + type: "boolean", + nullable: false, + defaultValue: false); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "is_moderator", + table: "group_pairs"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20221006122618_groupbans.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20221006122618_groupbans.Designer.cs new file mode 100644 index 0000000..9c82ebd --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20221006122618_groupbans.Designer.cs @@ -0,0 +1,462 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20221006122618_groupbans")] + partial class groupbans + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("BannedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_user_uid"); + + b.Property("BannedByUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_by_uid"); + + b.Property("BannedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("banned_on"); + + b.Property("BannedReason") + .HasColumnType("text") + .HasColumnName("banned_reason"); + + b.HasKey("GroupGID", "BannedUserUID") + .HasName("pk_group_bans"); + + b.HasIndex("BannedByUID") + .HasDatabaseName("ix_group_bans_banned_by_uid"); + + b.HasIndex("BannedUserUID") + .HasDatabaseName("ix_group_bans_banned_user_uid"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_bans_group_gid"); + + b.ToTable("group_bans", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id2"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id7"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.HasOne("MareSynchronosShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id4"); + + b.HasOne("MareSynchronosShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id5"); + + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_groups_group_temp_id"); + + b.Navigation("BannedBy"); + + b.Navigation("BannedUser"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id6"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20221006122618_groupbans.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20221006122618_groupbans.cs new file mode 100644 index 0000000..84860eb --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20221006122618_groupbans.cs @@ -0,0 +1,135 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + public partial class groupbans : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_group_pairs_groups_group_temp_id", + table: "group_pairs"); + + migrationBuilder.DropForeignKey( + name: "fk_group_pairs_users_group_user_temp_id4", + table: "group_pairs"); + + migrationBuilder.DropForeignKey( + name: "fk_groups_users_owner_temp_id5", + table: "groups"); + + migrationBuilder.CreateTable( + name: "group_bans", + columns: table => new + { + group_gid = table.Column(type: "character varying(20)", nullable: false), + banned_user_uid = table.Column(type: "character varying(10)", nullable: false), + banned_by_uid = table.Column(type: "character varying(10)", nullable: true), + banned_on = table.Column(type: "timestamp with time zone", nullable: false), + banned_reason = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_group_bans", x => new { x.group_gid, x.banned_user_uid }); + table.ForeignKey( + name: "fk_group_bans_groups_group_temp_id", + column: x => x.group_gid, + principalTable: "groups", + principalColumn: "gid", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_group_bans_users_banned_by_temp_id4", + column: x => x.banned_by_uid, + principalTable: "users", + principalColumn: "uid"); + table.ForeignKey( + name: "fk_group_bans_users_banned_user_temp_id5", + column: x => x.banned_user_uid, + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_group_bans_banned_by_uid", + table: "group_bans", + column: "banned_by_uid"); + + migrationBuilder.CreateIndex( + name: "ix_group_bans_banned_user_uid", + table: "group_bans", + column: "banned_user_uid"); + + migrationBuilder.CreateIndex( + name: "ix_group_bans_group_gid", + table: "group_bans", + column: "group_gid"); + + migrationBuilder.AddForeignKey( + name: "fk_group_pairs_groups_group_temp_id1", + table: "group_pairs", + column: "group_gid", + principalTable: "groups", + principalColumn: "gid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_group_pairs_users_group_user_temp_id6", + table: "group_pairs", + column: "group_user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_groups_users_owner_temp_id7", + table: "groups", + column: "owner_uid", + principalTable: "users", + principalColumn: "uid"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_group_pairs_groups_group_temp_id1", + table: "group_pairs"); + + migrationBuilder.DropForeignKey( + name: "fk_group_pairs_users_group_user_temp_id6", + table: "group_pairs"); + + migrationBuilder.DropForeignKey( + name: "fk_groups_users_owner_temp_id7", + table: "groups"); + + migrationBuilder.DropTable( + name: "group_bans"); + + migrationBuilder.AddForeignKey( + name: "fk_group_pairs_groups_group_temp_id", + table: "group_pairs", + column: "group_gid", + principalTable: "groups", + principalColumn: "gid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_group_pairs_users_group_user_temp_id4", + table: "group_pairs", + column: "group_user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_groups_users_owner_temp_id5", + table: "groups", + column: "owner_uid", + principalTable: "users", + principalColumn: "uid"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20221024141548_GroupTempInvite.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20221024141548_GroupTempInvite.Designer.cs new file mode 100644 index 0000000..8f085d2 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20221024141548_GroupTempInvite.Designer.cs @@ -0,0 +1,501 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20221024141548_GroupTempInvite")] + partial class GroupTempInvite + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("BannedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_user_uid"); + + b.Property("BannedByUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_by_uid"); + + b.Property("BannedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("banned_on"); + + b.Property("BannedReason") + .HasColumnType("text") + .HasColumnName("banned_reason"); + + b.HasKey("GroupGID", "BannedUserUID") + .HasName("pk_group_bans"); + + b.HasIndex("BannedByUID") + .HasDatabaseName("ix_group_bans_banned_by_uid"); + + b.HasIndex("BannedUserUID") + .HasDatabaseName("ix_group_bans_banned_user_uid"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_bans_group_gid"); + + b.ToTable("group_bans", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Invite") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("invite"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.HasKey("GroupGID", "Invite") + .HasName("pk_group_temp_invites"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_temp_invites_group_gid"); + + b.HasIndex("Invite") + .HasDatabaseName("ix_group_temp_invites_invite"); + + b.ToTable("group_temp_invites", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id2"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id7"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.HasOne("MareSynchronosShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id4"); + + b.HasOne("MareSynchronosShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id5"); + + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_groups_group_temp_id"); + + b.Navigation("BannedBy"); + + b.Navigation("BannedUser"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id6"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20221024141548_GroupTempInvite.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20221024141548_GroupTempInvite.cs new file mode 100644 index 0000000..8261909 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20221024141548_GroupTempInvite.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + public partial class GroupTempInvite : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "group_temp_invites", + columns: table => new + { + group_gid = table.Column(type: "character varying(20)", nullable: false), + invite = table.Column(type: "character varying(10)", maxLength: 10, nullable: false), + expiration_date = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_group_temp_invites", x => new { x.group_gid, x.invite }); + table.ForeignKey( + name: "fk_group_temp_invites_groups_group_gid", + column: x => x.group_gid, + principalTable: "groups", + principalColumn: "gid", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_group_temp_invites_group_gid", + table: "group_temp_invites", + column: "group_gid"); + + migrationBuilder.CreateIndex( + name: "ix_group_temp_invites_invite", + table: "group_temp_invites", + column: "invite"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "group_temp_invites"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20221024181912_AdjustInviteLength.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20221024181912_AdjustInviteLength.Designer.cs new file mode 100644 index 0000000..1cab514 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20221024181912_AdjustInviteLength.Designer.cs @@ -0,0 +1,501 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20221024181912_AdjustInviteLength")] + partial class AdjustInviteLength + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("BannedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_user_uid"); + + b.Property("BannedByUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_by_uid"); + + b.Property("BannedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("banned_on"); + + b.Property("BannedReason") + .HasColumnType("text") + .HasColumnName("banned_reason"); + + b.HasKey("GroupGID", "BannedUserUID") + .HasName("pk_group_bans"); + + b.HasIndex("BannedByUID") + .HasDatabaseName("ix_group_bans_banned_by_uid"); + + b.HasIndex("BannedUserUID") + .HasDatabaseName("ix_group_bans_banned_user_uid"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_bans_group_gid"); + + b.ToTable("group_bans", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Invite") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("invite"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.HasKey("GroupGID", "Invite") + .HasName("pk_group_temp_invites"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_temp_invites_group_gid"); + + b.HasIndex("Invite") + .HasDatabaseName("ix_group_temp_invites_invite"); + + b.ToTable("group_temp_invites", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id2"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id7"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.HasOne("MareSynchronosShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id4"); + + b.HasOne("MareSynchronosShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id5"); + + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_groups_group_temp_id"); + + b.Navigation("BannedBy"); + + b.Navigation("BannedUser"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id6"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20221024181912_AdjustInviteLength.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20221024181912_AdjustInviteLength.cs new file mode 100644 index 0000000..6ad4850 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20221024181912_AdjustInviteLength.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + public partial class AdjustInviteLength : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "invite", + table: "group_temp_invites", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(10)", + oldMaxLength: 10); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "invite", + table: "group_temp_invites", + type: "character varying(10)", + maxLength: 10, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20221228033214_FileCacheSize.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20221228033214_FileCacheSize.Designer.cs new file mode 100644 index 0000000..09c04cb --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20221228033214_FileCacheSize.Designer.cs @@ -0,0 +1,506 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20221228033214_FileCacheSize")] + partial class FileCacheSize + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("BannedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_user_uid"); + + b.Property("BannedByUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_by_uid"); + + b.Property("BannedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("banned_on"); + + b.Property("BannedReason") + .HasColumnType("text") + .HasColumnName("banned_reason"); + + b.HasKey("GroupGID", "BannedUserUID") + .HasName("pk_group_bans"); + + b.HasIndex("BannedByUID") + .HasDatabaseName("ix_group_bans_banned_by_uid"); + + b.HasIndex("BannedUserUID") + .HasDatabaseName("ix_group_bans_banned_user_uid"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_bans_group_gid"); + + b.ToTable("group_bans", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Invite") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("invite"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.HasKey("GroupGID", "Invite") + .HasName("pk_group_temp_invites"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_temp_invites_group_gid"); + + b.HasIndex("Invite") + .HasDatabaseName("ix_group_temp_invites_invite"); + + b.ToTable("group_temp_invites", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id2"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id7"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.HasOne("MareSynchronosShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id4"); + + b.HasOne("MareSynchronosShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id5"); + + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_groups_group_temp_id"); + + b.Navigation("BannedBy"); + + b.Navigation("BannedUser"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id6"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20221228033214_FileCacheSize.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20221228033214_FileCacheSize.cs new file mode 100644 index 0000000..8fc1201 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20221228033214_FileCacheSize.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + /// + public partial class FileCacheSize : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "size", + table: "file_caches", + type: "bigint", + nullable: false, + defaultValue: 0L); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "size", + table: "file_caches"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20230111092127_IsBannedForAuth.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20230111092127_IsBannedForAuth.Designer.cs new file mode 100644 index 0000000..7f9e0c6 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20230111092127_IsBannedForAuth.Designer.cs @@ -0,0 +1,510 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20230111092127_IsBannedForAuth")] + partial class IsBannedForAuth + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("IsBanned") + .HasColumnType("boolean") + .HasColumnName("is_banned"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("BannedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_user_uid"); + + b.Property("BannedByUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_by_uid"); + + b.Property("BannedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("banned_on"); + + b.Property("BannedReason") + .HasColumnType("text") + .HasColumnName("banned_reason"); + + b.HasKey("GroupGID", "BannedUserUID") + .HasName("pk_group_bans"); + + b.HasIndex("BannedByUID") + .HasDatabaseName("ix_group_bans_banned_by_uid"); + + b.HasIndex("BannedUserUID") + .HasDatabaseName("ix_group_bans_banned_user_uid"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_bans_group_gid"); + + b.ToTable("group_bans", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Invite") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("invite"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.HasKey("GroupGID", "Invite") + .HasName("pk_group_temp_invites"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_temp_invites_group_gid"); + + b.HasIndex("Invite") + .HasDatabaseName("ix_group_temp_invites_invite"); + + b.ToTable("group_temp_invites", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id2"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id7"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.HasOne("MareSynchronosShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id4"); + + b.HasOne("MareSynchronosShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id5"); + + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_groups_group_temp_id"); + + b.Navigation("BannedBy"); + + b.Navigation("BannedUser"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id6"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20230111092127_IsBannedForAuth.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20230111092127_IsBannedForAuth.cs new file mode 100644 index 0000000..7f8ab0e --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20230111092127_IsBannedForAuth.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + /// + public partial class IsBannedForAuth : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "is_banned", + table: "auth", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "is_banned", + table: "auth"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20230118184347_FilesUploadDate.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20230118184347_FilesUploadDate.Designer.cs new file mode 100644 index 0000000..1b5bad8 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20230118184347_FilesUploadDate.Designer.cs @@ -0,0 +1,514 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20230118184347_FilesUploadDate")] + partial class FilesUploadDate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("IsBanned") + .HasColumnType("boolean") + .HasColumnName("is_banned"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("UploadDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("upload_date"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("BannedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_user_uid"); + + b.Property("BannedByUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_by_uid"); + + b.Property("BannedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("banned_on"); + + b.Property("BannedReason") + .HasColumnType("text") + .HasColumnName("banned_reason"); + + b.HasKey("GroupGID", "BannedUserUID") + .HasName("pk_group_bans"); + + b.HasIndex("BannedByUID") + .HasDatabaseName("ix_group_bans_banned_by_uid"); + + b.HasIndex("BannedUserUID") + .HasDatabaseName("ix_group_bans_banned_user_uid"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_bans_group_gid"); + + b.ToTable("group_bans", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Invite") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("invite"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.HasKey("GroupGID", "Invite") + .HasName("pk_group_temp_invites"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_temp_invites_group_gid"); + + b.HasIndex("Invite") + .HasDatabaseName("ix_group_temp_invites_invite"); + + b.ToTable("group_temp_invites", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id2"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id7"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.HasOne("MareSynchronosShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id4"); + + b.HasOne("MareSynchronosShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id5"); + + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_groups_group_temp_id"); + + b.Navigation("BannedBy"); + + b.Navigation("BannedUser"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id6"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20230118184347_FilesUploadDate.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20230118184347_FilesUploadDate.cs new file mode 100644 index 0000000..8f6a46a --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20230118184347_FilesUploadDate.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + /// + public partial class FilesUploadDate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "upload_date", + table: "file_caches", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTime(2000, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "upload_date", + table: "file_caches"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20230126163758_GroupPerms.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20230126163758_GroupPerms.Designer.cs new file mode 100644 index 0000000..883a1c3 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20230126163758_GroupPerms.Designer.cs @@ -0,0 +1,530 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20230126163758_GroupPerms")] + partial class GroupPerms + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("IsBanned") + .HasColumnType("boolean") + .HasColumnName("is_banned"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("UploadDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("upload_date"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("BannedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_user_uid"); + + b.Property("BannedByUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_by_uid"); + + b.Property("BannedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("banned_on"); + + b.Property("BannedReason") + .HasColumnType("text") + .HasColumnName("banned_reason"); + + b.HasKey("GroupGID", "BannedUserUID") + .HasName("pk_group_bans"); + + b.HasIndex("BannedByUID") + .HasDatabaseName("ix_group_bans_banned_by_uid"); + + b.HasIndex("BannedUserUID") + .HasDatabaseName("ix_group_bans_banned_user_uid"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_bans_group_gid"); + + b.ToTable("group_bans", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Invite") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("invite"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.HasKey("GroupGID", "Invite") + .HasName("pk_group_temp_invites"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_temp_invites_group_gid"); + + b.HasIndex("Invite") + .HasDatabaseName("ix_group_temp_invites_invite"); + + b.ToTable("group_temp_invites", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id2"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id7"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.HasOne("MareSynchronosShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id4"); + + b.HasOne("MareSynchronosShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id5"); + + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_groups_group_temp_id"); + + b.Navigation("BannedBy"); + + b.Navigation("BannedUser"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id6"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20230126163758_GroupPerms.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20230126163758_GroupPerms.cs new file mode 100644 index 0000000..0729490 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20230126163758_GroupPerms.cs @@ -0,0 +1,62 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + /// + public partial class GroupPerms : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "disable_animations", + table: "groups", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "disable_sounds", + table: "groups", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "disable_animations", + table: "group_pairs", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "disable_sounds", + table: "group_pairs", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "disable_animations", + table: "groups"); + + migrationBuilder.DropColumn( + name: "disable_sounds", + table: "groups"); + + migrationBuilder.DropColumn( + name: "disable_animations", + table: "group_pairs"); + + migrationBuilder.DropColumn( + name: "disable_sounds", + table: "group_pairs"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20230131193425_AddPrimaryUserToAuth.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20230131193425_AddPrimaryUserToAuth.Designer.cs new file mode 100644 index 0000000..2baee23 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20230131193425_AddPrimaryUserToAuth.Designer.cs @@ -0,0 +1,544 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20230131193425_AddPrimaryUserToAuth")] + partial class AddPrimaryUserToAuth + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("IsBanned") + .HasColumnType("boolean") + .HasColumnName("is_banned"); + + b.Property("PrimaryUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("primary_user_uid"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("PrimaryUserUID") + .HasDatabaseName("ix_auth_primary_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("UploadDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("upload_date"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("BannedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_user_uid"); + + b.Property("BannedByUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_by_uid"); + + b.Property("BannedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("banned_on"); + + b.Property("BannedReason") + .HasColumnType("text") + .HasColumnName("banned_reason"); + + b.HasKey("GroupGID", "BannedUserUID") + .HasName("pk_group_bans"); + + b.HasIndex("BannedByUID") + .HasDatabaseName("ix_group_bans_banned_by_uid"); + + b.HasIndex("BannedUserUID") + .HasDatabaseName("ix_group_bans_banned_user_uid"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_bans_group_gid"); + + b.ToTable("group_bans", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Invite") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("invite"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.HasKey("GroupGID", "Invite") + .HasName("pk_group_temp_invites"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_temp_invites_group_gid"); + + b.HasIndex("Invite") + .HasDatabaseName("ix_group_temp_invites_invite"); + + b.ToTable("group_temp_invites", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_temp_id"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id1"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id2"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id3"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id8"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.HasOne("MareSynchronosShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id5"); + + b.HasOne("MareSynchronosShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id6"); + + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_groups_group_temp_id"); + + b.Navigation("BannedBy"); + + b.Navigation("BannedUser"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id7"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20230131193425_AddPrimaryUserToAuth.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20230131193425_AddPrimaryUserToAuth.cs new file mode 100644 index 0000000..488bbf0 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20230131193425_AddPrimaryUserToAuth.cs @@ -0,0 +1,210 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + /// + public partial class AddPrimaryUserToAuth : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_auth_users_user_temp_id", + table: "auth"); + + migrationBuilder.DropForeignKey( + name: "fk_client_pairs_users_other_user_temp_id1", + table: "client_pairs"); + + migrationBuilder.DropForeignKey( + name: "fk_client_pairs_users_user_temp_id2", + table: "client_pairs"); + + migrationBuilder.DropForeignKey( + name: "fk_group_bans_users_banned_by_temp_id4", + table: "group_bans"); + + migrationBuilder.DropForeignKey( + name: "fk_group_bans_users_banned_user_temp_id5", + table: "group_bans"); + + migrationBuilder.DropForeignKey( + name: "fk_group_pairs_users_group_user_temp_id6", + table: "group_pairs"); + + migrationBuilder.DropForeignKey( + name: "fk_groups_users_owner_temp_id7", + table: "groups"); + + migrationBuilder.AddColumn( + name: "primary_user_uid", + table: "auth", + type: "character varying(10)", + nullable: true); + + migrationBuilder.CreateIndex( + name: "ix_auth_primary_user_uid", + table: "auth", + column: "primary_user_uid"); + + migrationBuilder.AddForeignKey( + name: "fk_auth_users_primary_user_temp_id", + table: "auth", + column: "primary_user_uid", + principalTable: "users", + principalColumn: "uid"); + + migrationBuilder.AddForeignKey( + name: "fk_auth_users_user_temp_id1", + table: "auth", + column: "user_uid", + principalTable: "users", + principalColumn: "uid"); + + migrationBuilder.AddForeignKey( + name: "fk_client_pairs_users_other_user_temp_id2", + table: "client_pairs", + column: "other_user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_client_pairs_users_user_temp_id3", + table: "client_pairs", + column: "user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_group_bans_users_banned_by_temp_id5", + table: "group_bans", + column: "banned_by_uid", + principalTable: "users", + principalColumn: "uid"); + + migrationBuilder.AddForeignKey( + name: "fk_group_bans_users_banned_user_temp_id6", + table: "group_bans", + column: "banned_user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_group_pairs_users_group_user_temp_id7", + table: "group_pairs", + column: "group_user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_groups_users_owner_temp_id8", + table: "groups", + column: "owner_uid", + principalTable: "users", + principalColumn: "uid"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_auth_users_primary_user_temp_id", + table: "auth"); + + migrationBuilder.DropForeignKey( + name: "fk_auth_users_user_temp_id1", + table: "auth"); + + migrationBuilder.DropForeignKey( + name: "fk_client_pairs_users_other_user_temp_id2", + table: "client_pairs"); + + migrationBuilder.DropForeignKey( + name: "fk_client_pairs_users_user_temp_id3", + table: "client_pairs"); + + migrationBuilder.DropForeignKey( + name: "fk_group_bans_users_banned_by_temp_id5", + table: "group_bans"); + + migrationBuilder.DropForeignKey( + name: "fk_group_bans_users_banned_user_temp_id6", + table: "group_bans"); + + migrationBuilder.DropForeignKey( + name: "fk_group_pairs_users_group_user_temp_id7", + table: "group_pairs"); + + migrationBuilder.DropForeignKey( + name: "fk_groups_users_owner_temp_id8", + table: "groups"); + + migrationBuilder.DropIndex( + name: "ix_auth_primary_user_uid", + table: "auth"); + + migrationBuilder.DropColumn( + name: "primary_user_uid", + table: "auth"); + + migrationBuilder.AddForeignKey( + name: "fk_auth_users_user_temp_id", + table: "auth", + column: "user_uid", + principalTable: "users", + principalColumn: "uid"); + + migrationBuilder.AddForeignKey( + name: "fk_client_pairs_users_other_user_temp_id1", + table: "client_pairs", + column: "other_user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_client_pairs_users_user_temp_id2", + table: "client_pairs", + column: "user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_group_bans_users_banned_by_temp_id4", + table: "group_bans", + column: "banned_by_uid", + principalTable: "users", + principalColumn: "uid"); + + migrationBuilder.AddForeignKey( + name: "fk_group_bans_users_banned_user_temp_id5", + table: "group_bans", + column: "banned_user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_group_pairs_users_group_user_temp_id6", + table: "group_pairs", + column: "group_user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_groups_users_owner_temp_id7", + table: "groups", + column: "owner_uid", + principalTable: "users", + principalColumn: "uid"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20230228001033_UserPerms.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20230228001033_UserPerms.Designer.cs new file mode 100644 index 0000000..58f336e --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20230228001033_UserPerms.Designer.cs @@ -0,0 +1,552 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20230228001033_UserPerms")] + partial class UserPerms + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("IsBanned") + .HasColumnType("boolean") + .HasColumnName("is_banned"); + + b.Property("PrimaryUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("primary_user_uid"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("PrimaryUserUID") + .HasDatabaseName("ix_auth_primary_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("UploadDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("upload_date"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("BannedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_user_uid"); + + b.Property("BannedByUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_by_uid"); + + b.Property("BannedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("banned_on"); + + b.Property("BannedReason") + .HasColumnType("text") + .HasColumnName("banned_reason"); + + b.HasKey("GroupGID", "BannedUserUID") + .HasName("pk_group_bans"); + + b.HasIndex("BannedByUID") + .HasDatabaseName("ix_group_bans_banned_by_uid"); + + b.HasIndex("BannedUserUID") + .HasDatabaseName("ix_group_bans_banned_user_uid"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_bans_group_gid"); + + b.ToTable("group_bans", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Invite") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("invite"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.HasKey("GroupGID", "Invite") + .HasName("pk_group_temp_invites"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_temp_invites_group_gid"); + + b.HasIndex("Invite") + .HasDatabaseName("ix_group_temp_invites_invite"); + + b.ToTable("group_temp_invites", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_temp_id"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id1"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id2"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id3"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id8"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.HasOne("MareSynchronosShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id5"); + + b.HasOne("MareSynchronosShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id6"); + + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_groups_group_temp_id"); + + b.Navigation("BannedBy"); + + b.Navigation("BannedUser"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id7"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20230228001033_UserPerms.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20230228001033_UserPerms.cs new file mode 100644 index 0000000..18556ac --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20230228001033_UserPerms.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + /// + public partial class UserPerms : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "disable_animations", + table: "client_pairs", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "disable_sounds", + table: "client_pairs", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "disable_animations", + table: "client_pairs"); + + migrationBuilder.DropColumn( + name: "disable_sounds", + table: "client_pairs"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20230319015307_UserProfileData.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20230319015307_UserProfileData.Designer.cs new file mode 100644 index 0000000..ea2bac1 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20230319015307_UserProfileData.Designer.cs @@ -0,0 +1,584 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20230319015307_UserProfileData")] + partial class UserProfileData + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("IsBanned") + .HasColumnType("boolean") + .HasColumnName("is_banned"); + + b.Property("PrimaryUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("primary_user_uid"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("PrimaryUserUID") + .HasDatabaseName("ix_auth_primary_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("UploadDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("upload_date"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("BannedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_user_uid"); + + b.Property("BannedByUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_by_uid"); + + b.Property("BannedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("banned_on"); + + b.Property("BannedReason") + .HasColumnType("text") + .HasColumnName("banned_reason"); + + b.HasKey("GroupGID", "BannedUserUID") + .HasName("pk_group_bans"); + + b.HasIndex("BannedByUID") + .HasDatabaseName("ix_group_bans_banned_by_uid"); + + b.HasIndex("BannedUserUID") + .HasDatabaseName("ix_group_bans_banned_user_uid"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_bans_group_gid"); + + b.ToTable("group_bans", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Invite") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("invite"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.HasKey("GroupGID", "Invite") + .HasName("pk_group_temp_invites"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_temp_invites_group_gid"); + + b.HasIndex("Invite") + .HasDatabaseName("ix_group_temp_invites_invite"); + + b.ToTable("group_temp_invites", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileData", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("Base64ProfileImage") + .HasColumnType("text") + .HasColumnName("base64profile_image"); + + b.Property("UserDescription") + .HasColumnType("text") + .HasColumnName("user_description"); + + b.HasKey("UserUID") + .HasName("pk_user_profile_data"); + + b.ToTable("user_profile_data", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_temp_id"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id1"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id2"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id3"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id8"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.HasOne("MareSynchronosShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id5"); + + b.HasOne("MareSynchronosShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id6"); + + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_groups_group_temp_id"); + + b.Navigation("BannedBy"); + + b.Navigation("BannedUser"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id7"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileData", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20230319015307_UserProfileData.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20230319015307_UserProfileData.cs new file mode 100644 index 0000000..6b4bb1a --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20230319015307_UserProfileData.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + /// + public partial class UserProfileData : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "user_profile_data", + columns: table => new + { + user_uid = table.Column(type: "character varying(10)", nullable: false), + base64profile_image = table.Column(type: "text", nullable: true), + user_description = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_user_profile_data", x => x.user_uid); + table.ForeignKey( + name: "fk_user_profile_data_users_user_uid", + column: x => x.user_uid, + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "user_profile_data"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20230319114005_UserProfileReports.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20230319114005_UserProfileReports.Designer.cs new file mode 100644 index 0000000..4fd766a --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20230319114005_UserProfileReports.Designer.cs @@ -0,0 +1,650 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20230319114005_UserProfileReports")] + partial class UserProfileReports + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("IsBanned") + .HasColumnType("boolean") + .HasColumnName("is_banned"); + + b.Property("PrimaryUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("primary_user_uid"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("PrimaryUserUID") + .HasDatabaseName("ix_auth_primary_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("UploadDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("upload_date"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("BannedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_user_uid"); + + b.Property("BannedByUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_by_uid"); + + b.Property("BannedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("banned_on"); + + b.Property("BannedReason") + .HasColumnType("text") + .HasColumnName("banned_reason"); + + b.HasKey("GroupGID", "BannedUserUID") + .HasName("pk_group_bans"); + + b.HasIndex("BannedByUID") + .HasDatabaseName("ix_group_bans_banned_by_uid"); + + b.HasIndex("BannedUserUID") + .HasDatabaseName("ix_group_bans_banned_user_uid"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_bans_group_gid"); + + b.ToTable("group_bans", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Invite") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("invite"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.HasKey("GroupGID", "Invite") + .HasName("pk_group_temp_invites"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_temp_invites_group_gid"); + + b.HasIndex("Invite") + .HasDatabaseName("ix_group_temp_invites_invite"); + + b.ToTable("group_temp_invites", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileData", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("Base64ProfileImage") + .HasColumnType("text") + .HasColumnName("base64profile_image"); + + b.Property("FlaggedForReport") + .HasColumnType("boolean") + .HasColumnName("flagged_for_report"); + + b.Property("IsNSFW") + .HasColumnType("boolean") + .HasColumnName("is_nsfw"); + + b.Property("ProfileDisabled") + .HasColumnType("boolean") + .HasColumnName("profile_disabled"); + + b.Property("UserDescription") + .HasColumnType("text") + .HasColumnName("user_description"); + + b.HasKey("UserUID") + .HasName("pk_user_profile_data"); + + b.ToTable("user_profile_data", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileDataReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ReportDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("report_date"); + + b.Property("ReportReason") + .HasColumnType("text") + .HasColumnName("report_reason"); + + b.Property("ReportedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("reported_user_uid"); + + b.Property("ReportingUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("reporting_user_uid"); + + b.HasKey("Id") + .HasName("pk_user_profile_data_reports"); + + b.HasIndex("ReportedUserUID") + .HasDatabaseName("ix_user_profile_data_reports_reported_user_uid"); + + b.HasIndex("ReportingUserUID") + .HasDatabaseName("ix_user_profile_data_reports_reporting_user_uid"); + + b.ToTable("user_profile_data_reports", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_temp_id"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id1"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id2"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id3"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id8"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.HasOne("MareSynchronosShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id5"); + + b.HasOne("MareSynchronosShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id6"); + + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_groups_group_temp_id"); + + b.Navigation("BannedBy"); + + b.Navigation("BannedUser"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id7"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileData", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileDataReport", b => + { + b.HasOne("MareSynchronosShared.Models.User", "ReportedUser") + .WithMany() + .HasForeignKey("ReportedUserUID") + .HasConstraintName("fk_user_profile_data_reports_users_reported_user_uid"); + + b.HasOne("MareSynchronosShared.Models.User", "ReportingUser") + .WithMany() + .HasForeignKey("ReportingUserUID") + .HasConstraintName("fk_user_profile_data_reports_users_reporting_user_uid"); + + b.Navigation("ReportedUser"); + + b.Navigation("ReportingUser"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20230319114005_UserProfileReports.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20230319114005_UserProfileReports.cs new file mode 100644 index 0000000..660af57 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20230319114005_UserProfileReports.cs @@ -0,0 +1,92 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + /// + public partial class UserProfileReports : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "flagged_for_report", + table: "user_profile_data", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "is_nsfw", + table: "user_profile_data", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "profile_disabled", + table: "user_profile_data", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.CreateTable( + name: "user_profile_data_reports", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + report_date = table.Column(type: "timestamp with time zone", nullable: false), + reported_user_uid = table.Column(type: "character varying(10)", nullable: true), + reporting_user_uid = table.Column(type: "character varying(10)", nullable: true), + report_reason = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_user_profile_data_reports", x => x.id); + table.ForeignKey( + name: "fk_user_profile_data_reports_users_reported_user_uid", + column: x => x.reported_user_uid, + principalTable: "users", + principalColumn: "uid"); + table.ForeignKey( + name: "fk_user_profile_data_reports_users_reporting_user_uid", + column: x => x.reporting_user_uid, + principalTable: "users", + principalColumn: "uid"); + }); + + migrationBuilder.CreateIndex( + name: "ix_user_profile_data_reports_reported_user_uid", + table: "user_profile_data_reports", + column: "reported_user_uid"); + + migrationBuilder.CreateIndex( + name: "ix_user_profile_data_reports_reporting_user_uid", + table: "user_profile_data_reports", + column: "reporting_user_uid"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "user_profile_data_reports"); + + migrationBuilder.DropColumn( + name: "flagged_for_report", + table: "user_profile_data"); + + migrationBuilder.DropColumn( + name: "is_nsfw", + table: "user_profile_data"); + + migrationBuilder.DropColumn( + name: "profile_disabled", + table: "user_profile_data"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20230420075153_DisableVFX.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20230420075153_DisableVFX.Designer.cs new file mode 100644 index 0000000..df61fe6 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20230420075153_DisableVFX.Designer.cs @@ -0,0 +1,662 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20230420075153_DisableVFX")] + partial class DisableVFX + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("IsBanned") + .HasColumnType("boolean") + .HasColumnName("is_banned"); + + b.Property("PrimaryUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("primary_user_uid"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("PrimaryUserUID") + .HasDatabaseName("ix_auth_primary_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("DisableVFX") + .HasColumnType("boolean") + .HasColumnName("disable_vfx"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("UploadDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("upload_date"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("DisableVFX") + .HasColumnType("boolean") + .HasColumnName("disable_vfx"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("BannedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_user_uid"); + + b.Property("BannedByUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_by_uid"); + + b.Property("BannedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("banned_on"); + + b.Property("BannedReason") + .HasColumnType("text") + .HasColumnName("banned_reason"); + + b.HasKey("GroupGID", "BannedUserUID") + .HasName("pk_group_bans"); + + b.HasIndex("BannedByUID") + .HasDatabaseName("ix_group_bans_banned_by_uid"); + + b.HasIndex("BannedUserUID") + .HasDatabaseName("ix_group_bans_banned_user_uid"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_bans_group_gid"); + + b.ToTable("group_bans", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("DisableVFX") + .HasColumnType("boolean") + .HasColumnName("disable_vfx"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Invite") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("invite"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.HasKey("GroupGID", "Invite") + .HasName("pk_group_temp_invites"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_temp_invites_group_gid"); + + b.HasIndex("Invite") + .HasDatabaseName("ix_group_temp_invites_invite"); + + b.ToTable("group_temp_invites", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileData", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("Base64ProfileImage") + .HasColumnType("text") + .HasColumnName("base64profile_image"); + + b.Property("FlaggedForReport") + .HasColumnType("boolean") + .HasColumnName("flagged_for_report"); + + b.Property("IsNSFW") + .HasColumnType("boolean") + .HasColumnName("is_nsfw"); + + b.Property("ProfileDisabled") + .HasColumnType("boolean") + .HasColumnName("profile_disabled"); + + b.Property("UserDescription") + .HasColumnType("text") + .HasColumnName("user_description"); + + b.HasKey("UserUID") + .HasName("pk_user_profile_data"); + + b.ToTable("user_profile_data", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileDataReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ReportDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("report_date"); + + b.Property("ReportReason") + .HasColumnType("text") + .HasColumnName("report_reason"); + + b.Property("ReportedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("reported_user_uid"); + + b.Property("ReportingUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("reporting_user_uid"); + + b.HasKey("Id") + .HasName("pk_user_profile_data_reports"); + + b.HasIndex("ReportedUserUID") + .HasDatabaseName("ix_user_profile_data_reports_reported_user_uid"); + + b.HasIndex("ReportingUserUID") + .HasDatabaseName("ix_user_profile_data_reports_reporting_user_uid"); + + b.ToTable("user_profile_data_reports", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_temp_id"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id1"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id2"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id3"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id8"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.HasOne("MareSynchronosShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id5"); + + b.HasOne("MareSynchronosShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id6"); + + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_groups_group_temp_id"); + + b.Navigation("BannedBy"); + + b.Navigation("BannedUser"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id7"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileData", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileDataReport", b => + { + b.HasOne("MareSynchronosShared.Models.User", "ReportedUser") + .WithMany() + .HasForeignKey("ReportedUserUID") + .HasConstraintName("fk_user_profile_data_reports_users_reported_user_uid"); + + b.HasOne("MareSynchronosShared.Models.User", "ReportingUser") + .WithMany() + .HasForeignKey("ReportingUserUID") + .HasConstraintName("fk_user_profile_data_reports_users_reporting_user_uid"); + + b.Navigation("ReportedUser"); + + b.Navigation("ReportingUser"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20230420075153_DisableVFX.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20230420075153_DisableVFX.cs new file mode 100644 index 0000000..ca51b6e --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20230420075153_DisableVFX.cs @@ -0,0 +1,51 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + /// + public partial class DisableVFX : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "disable_vfx", + table: "groups", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "disable_vfx", + table: "group_pairs", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "disable_vfx", + table: "client_pairs", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "disable_vfx", + table: "groups"); + + migrationBuilder.DropColumn( + name: "disable_vfx", + table: "group_pairs"); + + migrationBuilder.DropColumn( + name: "disable_vfx", + table: "client_pairs"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20250627174017_Bump.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20250627174017_Bump.Designer.cs new file mode 100644 index 0000000..9727ed0 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20250627174017_Bump.Designer.cs @@ -0,0 +1,662 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20250627174017_Bump")] + partial class Bump + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("IsBanned") + .HasColumnType("boolean") + .HasColumnName("is_banned"); + + b.Property("PrimaryUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("primary_user_uid"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("PrimaryUserUID") + .HasDatabaseName("ix_auth_primary_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("DisableVFX") + .HasColumnType("boolean") + .HasColumnName("disable_vfx"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("UploadDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("upload_date"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("DisableVFX") + .HasColumnType("boolean") + .HasColumnName("disable_vfx"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("BannedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_user_uid"); + + b.Property("BannedByUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_by_uid"); + + b.Property("BannedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("banned_on"); + + b.Property("BannedReason") + .HasColumnType("text") + .HasColumnName("banned_reason"); + + b.HasKey("GroupGID", "BannedUserUID") + .HasName("pk_group_bans"); + + b.HasIndex("BannedByUID") + .HasDatabaseName("ix_group_bans_banned_by_uid"); + + b.HasIndex("BannedUserUID") + .HasDatabaseName("ix_group_bans_banned_user_uid"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_bans_group_gid"); + + b.ToTable("group_bans", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("DisableVFX") + .HasColumnType("boolean") + .HasColumnName("disable_vfx"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Invite") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("invite"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.HasKey("GroupGID", "Invite") + .HasName("pk_group_temp_invites"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_temp_invites_group_gid"); + + b.HasIndex("Invite") + .HasDatabaseName("ix_group_temp_invites_invite"); + + b.ToTable("group_temp_invites", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileData", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("Base64ProfileImage") + .HasColumnType("text") + .HasColumnName("base64profile_image"); + + b.Property("FlaggedForReport") + .HasColumnType("boolean") + .HasColumnName("flagged_for_report"); + + b.Property("IsNSFW") + .HasColumnType("boolean") + .HasColumnName("is_nsfw"); + + b.Property("ProfileDisabled") + .HasColumnType("boolean") + .HasColumnName("profile_disabled"); + + b.Property("UserDescription") + .HasColumnType("text") + .HasColumnName("user_description"); + + b.HasKey("UserUID") + .HasName("pk_user_profile_data"); + + b.ToTable("user_profile_data", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileDataReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ReportDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("report_date"); + + b.Property("ReportReason") + .HasColumnType("text") + .HasColumnName("report_reason"); + + b.Property("ReportedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("reported_user_uid"); + + b.Property("ReportingUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("reporting_user_uid"); + + b.HasKey("Id") + .HasName("pk_user_profile_data_reports"); + + b.HasIndex("ReportedUserUID") + .HasDatabaseName("ix_user_profile_data_reports_reported_user_uid"); + + b.HasIndex("ReportingUserUID") + .HasDatabaseName("ix_user_profile_data_reports_reporting_user_uid"); + + b.ToTable("user_profile_data_reports", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_uid"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_uid"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_uid"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_uid"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_uid"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.HasOne("MareSynchronosShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_uid"); + + b.HasOne("MareSynchronosShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_uid"); + + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_groups_group_gid"); + + b.Navigation("BannedBy"); + + b.Navigation("BannedUser"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_gid"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_uid"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileData", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileDataReport", b => + { + b.HasOne("MareSynchronosShared.Models.User", "ReportedUser") + .WithMany() + .HasForeignKey("ReportedUserUID") + .HasConstraintName("fk_user_profile_data_reports_users_reported_user_uid"); + + b.HasOne("MareSynchronosShared.Models.User", "ReportingUser") + .WithMany() + .HasForeignKey("ReportingUserUID") + .HasConstraintName("fk_user_profile_data_reports_users_reporting_user_uid"); + + b.Navigation("ReportedUser"); + + b.Navigation("ReportingUser"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20250627174017_Bump.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20250627174017_Bump.cs new file mode 100644 index 0000000..073330f --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20250627174017_Bump.cs @@ -0,0 +1,250 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + /// + public partial class Bump : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_auth_users_primary_user_temp_id", + table: "auth"); + + migrationBuilder.DropForeignKey( + name: "fk_auth_users_user_temp_id1", + table: "auth"); + + migrationBuilder.DropForeignKey( + name: "fk_client_pairs_users_other_user_temp_id2", + table: "client_pairs"); + + migrationBuilder.DropForeignKey( + name: "fk_client_pairs_users_user_temp_id3", + table: "client_pairs"); + + migrationBuilder.DropForeignKey( + name: "fk_group_bans_groups_group_temp_id", + table: "group_bans"); + + migrationBuilder.DropForeignKey( + name: "fk_group_bans_users_banned_by_temp_id5", + table: "group_bans"); + + migrationBuilder.DropForeignKey( + name: "fk_group_bans_users_banned_user_temp_id6", + table: "group_bans"); + + migrationBuilder.DropForeignKey( + name: "fk_group_pairs_groups_group_temp_id1", + table: "group_pairs"); + + migrationBuilder.DropForeignKey( + name: "fk_group_pairs_users_group_user_temp_id7", + table: "group_pairs"); + + migrationBuilder.DropForeignKey( + name: "fk_groups_users_owner_temp_id8", + table: "groups"); + + migrationBuilder.AddForeignKey( + name: "fk_auth_users_primary_user_uid", + table: "auth", + column: "primary_user_uid", + principalTable: "users", + principalColumn: "uid"); + + migrationBuilder.AddForeignKey( + name: "fk_auth_users_user_uid", + table: "auth", + column: "user_uid", + principalTable: "users", + principalColumn: "uid"); + + migrationBuilder.AddForeignKey( + name: "fk_client_pairs_users_other_user_uid", + table: "client_pairs", + column: "other_user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_client_pairs_users_user_uid", + table: "client_pairs", + column: "user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_group_bans_groups_group_gid", + table: "group_bans", + column: "group_gid", + principalTable: "groups", + principalColumn: "gid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_group_bans_users_banned_by_uid", + table: "group_bans", + column: "banned_by_uid", + principalTable: "users", + principalColumn: "uid"); + + migrationBuilder.AddForeignKey( + name: "fk_group_bans_users_banned_user_uid", + table: "group_bans", + column: "banned_user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_group_pairs_groups_group_gid", + table: "group_pairs", + column: "group_gid", + principalTable: "groups", + principalColumn: "gid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_group_pairs_users_group_user_uid", + table: "group_pairs", + column: "group_user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_groups_users_owner_uid", + table: "groups", + column: "owner_uid", + principalTable: "users", + principalColumn: "uid"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_auth_users_primary_user_uid", + table: "auth"); + + migrationBuilder.DropForeignKey( + name: "fk_auth_users_user_uid", + table: "auth"); + + migrationBuilder.DropForeignKey( + name: "fk_client_pairs_users_other_user_uid", + table: "client_pairs"); + + migrationBuilder.DropForeignKey( + name: "fk_client_pairs_users_user_uid", + table: "client_pairs"); + + migrationBuilder.DropForeignKey( + name: "fk_group_bans_groups_group_gid", + table: "group_bans"); + + migrationBuilder.DropForeignKey( + name: "fk_group_bans_users_banned_by_uid", + table: "group_bans"); + + migrationBuilder.DropForeignKey( + name: "fk_group_bans_users_banned_user_uid", + table: "group_bans"); + + migrationBuilder.DropForeignKey( + name: "fk_group_pairs_groups_group_gid", + table: "group_pairs"); + + migrationBuilder.DropForeignKey( + name: "fk_group_pairs_users_group_user_uid", + table: "group_pairs"); + + migrationBuilder.DropForeignKey( + name: "fk_groups_users_owner_uid", + table: "groups"); + + migrationBuilder.AddForeignKey( + name: "fk_auth_users_primary_user_temp_id", + table: "auth", + column: "primary_user_uid", + principalTable: "users", + principalColumn: "uid"); + + migrationBuilder.AddForeignKey( + name: "fk_auth_users_user_temp_id1", + table: "auth", + column: "user_uid", + principalTable: "users", + principalColumn: "uid"); + + migrationBuilder.AddForeignKey( + name: "fk_client_pairs_users_other_user_temp_id2", + table: "client_pairs", + column: "other_user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_client_pairs_users_user_temp_id3", + table: "client_pairs", + column: "user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_group_bans_groups_group_temp_id", + table: "group_bans", + column: "group_gid", + principalTable: "groups", + principalColumn: "gid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_group_bans_users_banned_by_temp_id5", + table: "group_bans", + column: "banned_by_uid", + principalTable: "users", + principalColumn: "uid"); + + migrationBuilder.AddForeignKey( + name: "fk_group_bans_users_banned_user_temp_id6", + table: "group_bans", + column: "banned_user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_group_pairs_groups_group_temp_id1", + table: "group_pairs", + column: "group_gid", + principalTable: "groups", + principalColumn: "gid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_group_pairs_users_group_user_temp_id7", + table: "group_pairs", + column: "group_user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_groups_users_owner_temp_id8", + table: "groups", + column: "owner_uid", + principalTable: "users", + principalColumn: "uid"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20250627174541_MCDO.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20250627174541_MCDO.Designer.cs new file mode 100644 index 0000000..0a9b2a6 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20250627174541_MCDO.Designer.cs @@ -0,0 +1,974 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20250627174541_MCDO")] + partial class MCDO + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("IsBanned") + .HasColumnType("boolean") + .HasColumnName("is_banned"); + + b.Property("PrimaryUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("primary_user_uid"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("PrimaryUserUID") + .HasDatabaseName("ix_auth_primary_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaData", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("UploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.Property("AccessType") + .HasColumnType("integer") + .HasColumnName("access_type"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_date"); + + b.Property("CustomizeData") + .HasColumnType("text") + .HasColumnName("customize_data"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("DownloadCount") + .HasColumnType("integer") + .HasColumnName("download_count"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property("GlamourerData") + .HasColumnType("text") + .HasColumnName("glamourer_data"); + + b.Property("ManipulationData") + .HasColumnType("text") + .HasColumnName("manipulation_data"); + + b.Property("ShareType") + .HasColumnType("integer") + .HasColumnName("share_type"); + + b.Property("UpdatedDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_date"); + + b.HasKey("Id", "UploaderUID") + .HasName("pk_chara_data"); + + b.HasIndex("Id") + .HasDatabaseName("ix_chara_data_id"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_chara_data_uploader_uid"); + + b.ToTable("chara_data", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataAllowance", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("AllowedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("allowed_user_uid"); + + b.HasKey("ParentId", "ParentUploaderUID", "AllowedUserUID") + .HasName("pk_chara_data_allowance"); + + b.HasIndex("AllowedUserUID") + .HasDatabaseName("ix_chara_data_allowance_allowed_user_uid"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_allowance_parent_id"); + + b.ToTable("chara_data_allowance", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataFile", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("GamePath") + .HasColumnType("text") + .HasColumnName("game_path"); + + b.Property("FileCacheHash") + .HasColumnType("character varying(40)") + .HasColumnName("file_cache_hash"); + + b.HasKey("ParentId", "ParentUploaderUID", "GamePath") + .HasName("pk_chara_data_files"); + + b.HasIndex("FileCacheHash") + .HasDatabaseName("ix_chara_data_files_file_cache_hash"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_files_parent_id"); + + b.ToTable("chara_data_files", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataFileSwap", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("GamePath") + .HasColumnType("text") + .HasColumnName("game_path"); + + b.Property("FilePath") + .HasColumnType("text") + .HasColumnName("file_path"); + + b.HasKey("ParentId", "ParentUploaderUID", "GamePath") + .HasName("pk_chara_data_file_swaps"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_file_swaps_parent_id"); + + b.ToTable("chara_data_file_swaps", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataOriginalFile", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("GamePath") + .HasColumnType("text") + .HasColumnName("game_path"); + + b.Property("Hash") + .HasColumnType("text") + .HasColumnName("hash"); + + b.HasKey("ParentId", "ParentUploaderUID", "GamePath") + .HasName("pk_chara_data_orig_files"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_orig_files_parent_id"); + + b.ToTable("chara_data_orig_files", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataPose", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("PoseData") + .HasColumnType("text") + .HasColumnName("pose_data"); + + b.Property("WorldData") + .HasColumnType("text") + .HasColumnName("world_data"); + + b.HasKey("ParentId", "ParentUploaderUID", "Id") + .HasName("pk_chara_data_poses"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_poses_parent_id"); + + b.ToTable("chara_data_poses", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("DisableVFX") + .HasColumnType("boolean") + .HasColumnName("disable_vfx"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("UploadDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("upload_date"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("DisableVFX") + .HasColumnType("boolean") + .HasColumnName("disable_vfx"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("BannedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_user_uid"); + + b.Property("BannedByUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_by_uid"); + + b.Property("BannedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("banned_on"); + + b.Property("BannedReason") + .HasColumnType("text") + .HasColumnName("banned_reason"); + + b.HasKey("GroupGID", "BannedUserUID") + .HasName("pk_group_bans"); + + b.HasIndex("BannedByUID") + .HasDatabaseName("ix_group_bans_banned_by_uid"); + + b.HasIndex("BannedUserUID") + .HasDatabaseName("ix_group_bans_banned_user_uid"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_bans_group_gid"); + + b.ToTable("group_bans", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("DisableVFX") + .HasColumnType("boolean") + .HasColumnName("disable_vfx"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Invite") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("invite"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.HasKey("GroupGID", "Invite") + .HasName("pk_group_temp_invites"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_temp_invites_group_gid"); + + b.HasIndex("Invite") + .HasDatabaseName("ix_group_temp_invites_invite"); + + b.ToTable("group_temp_invites", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileData", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("Base64ProfileImage") + .HasColumnType("text") + .HasColumnName("base64profile_image"); + + b.Property("FlaggedForReport") + .HasColumnType("boolean") + .HasColumnName("flagged_for_report"); + + b.Property("IsNSFW") + .HasColumnType("boolean") + .HasColumnName("is_nsfw"); + + b.Property("ProfileDisabled") + .HasColumnType("boolean") + .HasColumnName("profile_disabled"); + + b.Property("UserDescription") + .HasColumnType("text") + .HasColumnName("user_description"); + + b.HasKey("UserUID") + .HasName("pk_user_profile_data"); + + b.ToTable("user_profile_data", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileDataReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ReportDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("report_date"); + + b.Property("ReportReason") + .HasColumnType("text") + .HasColumnName("report_reason"); + + b.Property("ReportedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("reported_user_uid"); + + b.Property("ReportingUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("reporting_user_uid"); + + b.HasKey("Id") + .HasName("pk_user_profile_data_reports"); + + b.HasIndex("ReportedUserUID") + .HasDatabaseName("ix_user_profile_data_reports_reported_user_uid"); + + b.HasIndex("ReportingUserUID") + .HasDatabaseName("ix_user_profile_data_reports_reporting_user_uid"); + + b.ToTable("user_profile_data_reports", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_uid"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_uid"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaData", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataAllowance", b => + { + b.HasOne("MareSynchronosShared.Models.User", "AllowedUser") + .WithMany() + .HasForeignKey("AllowedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_allowance_users_allowed_user_uid"); + + b.HasOne("MareSynchronosShared.Models.CharaData", "Parent") + .WithMany("AllowedIndividiuals") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_allowance_chara_data_parent_id_parent_uploader_u"); + + b.Navigation("AllowedUser"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataFile", b => + { + b.HasOne("MareSynchronosShared.Models.FileCache", "FileCache") + .WithMany() + .HasForeignKey("FileCacheHash") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_files_files_file_cache_hash"); + + b.HasOne("MareSynchronosShared.Models.CharaData", "Parent") + .WithMany("Files") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_files_chara_data_parent_id_parent_uploader_uid"); + + b.Navigation("FileCache"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataFileSwap", b => + { + b.HasOne("MareSynchronosShared.Models.CharaData", "Parent") + .WithMany("FileSwaps") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_file_swaps_chara_data_parent_id_parent_uploader_"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataOriginalFile", b => + { + b.HasOne("MareSynchronosShared.Models.CharaData", "Parent") + .WithMany("OriginalFiles") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_orig_files_chara_data_parent_id_parent_uploader_"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataPose", b => + { + b.HasOne("MareSynchronosShared.Models.CharaData", "Parent") + .WithMany("Poses") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_poses_chara_data_parent_id_parent_uploader_uid"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_uid"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_uid"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_uid"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.HasOne("MareSynchronosShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_uid"); + + b.HasOne("MareSynchronosShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_uid"); + + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_groups_group_gid"); + + b.Navigation("BannedBy"); + + b.Navigation("BannedUser"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_gid"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_uid"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileData", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileDataReport", b => + { + b.HasOne("MareSynchronosShared.Models.User", "ReportedUser") + .WithMany() + .HasForeignKey("ReportedUserUID") + .HasConstraintName("fk_user_profile_data_reports_users_reported_user_uid"); + + b.HasOne("MareSynchronosShared.Models.User", "ReportingUser") + .WithMany() + .HasForeignKey("ReportingUserUID") + .HasConstraintName("fk_user_profile_data_reports_users_reporting_user_uid"); + + b.Navigation("ReportedUser"); + + b.Navigation("ReportingUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaData", b => + { + b.Navigation("AllowedIndividiuals"); + + b.Navigation("FileSwaps"); + + b.Navigation("Files"); + + b.Navigation("OriginalFiles"); + + b.Navigation("Poses"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20250627174541_MCDO.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20250627174541_MCDO.cs new file mode 100644 index 0000000..5466ead --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20250627174541_MCDO.cs @@ -0,0 +1,225 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + /// + public partial class MCDO : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "chara_data", + columns: table => new + { + id = table.Column(type: "text", nullable: false), + uploader_uid = table.Column(type: "character varying(10)", nullable: false), + created_date = table.Column(type: "timestamp with time zone", nullable: false), + updated_date = table.Column(type: "timestamp with time zone", nullable: false), + description = table.Column(type: "text", nullable: true), + access_type = table.Column(type: "integer", nullable: false), + share_type = table.Column(type: "integer", nullable: false), + expiry_date = table.Column(type: "timestamp with time zone", nullable: true), + glamourer_data = table.Column(type: "text", nullable: true), + customize_data = table.Column(type: "text", nullable: true), + manipulation_data = table.Column(type: "text", nullable: true), + download_count = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_chara_data", x => new { x.id, x.uploader_uid }); + table.ForeignKey( + name: "fk_chara_data_users_uploader_uid", + column: x => x.uploader_uid, + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "chara_data_allowance", + columns: table => new + { + parent_id = table.Column(type: "text", nullable: false), + parent_uploader_uid = table.Column(type: "character varying(10)", nullable: false), + allowed_user_uid = table.Column(type: "character varying(10)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_chara_data_allowance", x => new { x.parent_id, x.parent_uploader_uid, x.allowed_user_uid }); + table.ForeignKey( + name: "fk_chara_data_allowance_chara_data_parent_id_parent_uploader_u", + columns: x => new { x.parent_id, x.parent_uploader_uid }, + principalTable: "chara_data", + principalColumns: new[] { "id", "uploader_uid" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_chara_data_allowance_users_allowed_user_uid", + column: x => x.allowed_user_uid, + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "chara_data_file_swaps", + columns: table => new + { + parent_id = table.Column(type: "text", nullable: false), + parent_uploader_uid = table.Column(type: "character varying(10)", nullable: false), + game_path = table.Column(type: "text", nullable: false), + file_path = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_chara_data_file_swaps", x => new { x.parent_id, x.parent_uploader_uid, x.game_path }); + table.ForeignKey( + name: "fk_chara_data_file_swaps_chara_data_parent_id_parent_uploader_", + columns: x => new { x.parent_id, x.parent_uploader_uid }, + principalTable: "chara_data", + principalColumns: new[] { "id", "uploader_uid" }, + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "chara_data_files", + columns: table => new + { + game_path = table.Column(type: "text", nullable: false), + parent_id = table.Column(type: "text", nullable: false), + parent_uploader_uid = table.Column(type: "character varying(10)", nullable: false), + file_cache_hash = table.Column(type: "character varying(40)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_chara_data_files", x => new { x.parent_id, x.parent_uploader_uid, x.game_path }); + table.ForeignKey( + name: "fk_chara_data_files_chara_data_parent_id_parent_uploader_uid", + columns: x => new { x.parent_id, x.parent_uploader_uid }, + principalTable: "chara_data", + principalColumns: new[] { "id", "uploader_uid" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_chara_data_files_files_file_cache_hash", + column: x => x.file_cache_hash, + principalTable: "file_caches", + principalColumn: "hash", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "chara_data_orig_files", + columns: table => new + { + parent_id = table.Column(type: "text", nullable: false), + parent_uploader_uid = table.Column(type: "character varying(10)", nullable: false), + game_path = table.Column(type: "text", nullable: false), + hash = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_chara_data_orig_files", x => new { x.parent_id, x.parent_uploader_uid, x.game_path }); + table.ForeignKey( + name: "fk_chara_data_orig_files_chara_data_parent_id_parent_uploader_", + columns: x => new { x.parent_id, x.parent_uploader_uid }, + principalTable: "chara_data", + principalColumns: new[] { "id", "uploader_uid" }, + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "chara_data_poses", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + parent_id = table.Column(type: "text", nullable: false), + parent_uploader_uid = table.Column(type: "character varying(10)", nullable: false), + description = table.Column(type: "text", nullable: true), + pose_data = table.Column(type: "text", nullable: true), + world_data = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_chara_data_poses", x => new { x.parent_id, x.parent_uploader_uid, x.id }); + table.ForeignKey( + name: "fk_chara_data_poses_chara_data_parent_id_parent_uploader_uid", + columns: x => new { x.parent_id, x.parent_uploader_uid }, + principalTable: "chara_data", + principalColumns: new[] { "id", "uploader_uid" }, + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_chara_data_id", + table: "chara_data", + column: "id"); + + migrationBuilder.CreateIndex( + name: "ix_chara_data_uploader_uid", + table: "chara_data", + column: "uploader_uid"); + + migrationBuilder.CreateIndex( + name: "ix_chara_data_allowance_allowed_user_uid", + table: "chara_data_allowance", + column: "allowed_user_uid"); + + migrationBuilder.CreateIndex( + name: "ix_chara_data_allowance_parent_id", + table: "chara_data_allowance", + column: "parent_id"); + + migrationBuilder.CreateIndex( + name: "ix_chara_data_file_swaps_parent_id", + table: "chara_data_file_swaps", + column: "parent_id"); + + migrationBuilder.CreateIndex( + name: "ix_chara_data_files_file_cache_hash", + table: "chara_data_files", + column: "file_cache_hash"); + + migrationBuilder.CreateIndex( + name: "ix_chara_data_files_parent_id", + table: "chara_data_files", + column: "parent_id"); + + migrationBuilder.CreateIndex( + name: "ix_chara_data_orig_files_parent_id", + table: "chara_data_orig_files", + column: "parent_id"); + + migrationBuilder.CreateIndex( + name: "ix_chara_data_poses_parent_id", + table: "chara_data_poses", + column: "parent_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "chara_data_allowance"); + + migrationBuilder.DropTable( + name: "chara_data_file_swaps"); + + migrationBuilder.DropTable( + name: "chara_data_files"); + + migrationBuilder.DropTable( + name: "chara_data_orig_files"); + + migrationBuilder.DropTable( + name: "chara_data_poses"); + + migrationBuilder.DropTable( + name: "chara_data"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20250627204223_AllowedGroup.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20250627204223_AllowedGroup.Designer.cs new file mode 100644 index 0000000..d462c65 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20250627204223_AllowedGroup.Designer.cs @@ -0,0 +1,1094 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20250627204223_AllowedGroup")] + partial class AllowedGroup + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("IsBanned") + .HasColumnType("boolean") + .HasColumnName("is_banned"); + + b.Property("MarkForBan") + .HasColumnType("boolean") + .HasColumnName("mark_for_ban"); + + b.Property("PrimaryUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("primary_user_uid"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("PrimaryUserUID") + .HasDatabaseName("ix_auth_primary_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaData", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("UploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.Property("AccessType") + .HasColumnType("integer") + .HasColumnName("access_type"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_date"); + + b.Property("CustomizeData") + .HasColumnType("text") + .HasColumnName("customize_data"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("DownloadCount") + .HasColumnType("integer") + .HasColumnName("download_count"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property("GlamourerData") + .HasColumnType("text") + .HasColumnName("glamourer_data"); + + b.Property("ManipulationData") + .HasColumnType("text") + .HasColumnName("manipulation_data"); + + b.Property("ShareType") + .HasColumnType("integer") + .HasColumnName("share_type"); + + b.Property("UpdatedDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_date"); + + b.HasKey("Id", "UploaderUID") + .HasName("pk_chara_data"); + + b.HasIndex("Id") + .HasDatabaseName("ix_chara_data_id"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_chara_data_uploader_uid"); + + b.ToTable("chara_data", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataAllowance", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowedGroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("allowed_group_gid"); + + b.Property("AllowedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("allowed_user_uid"); + + b.HasKey("ParentId", "ParentUploaderUID", "Id") + .HasName("pk_chara_data_allowance"); + + b.HasIndex("AllowedGroupGID") + .HasDatabaseName("ix_chara_data_allowance_allowed_group_gid"); + + b.HasIndex("AllowedUserUID") + .HasDatabaseName("ix_chara_data_allowance_allowed_user_uid"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_allowance_parent_id"); + + b.ToTable("chara_data_allowance", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataFile", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("GamePath") + .HasColumnType("text") + .HasColumnName("game_path"); + + b.Property("FileCacheHash") + .HasColumnType("character varying(40)") + .HasColumnName("file_cache_hash"); + + b.HasKey("ParentId", "ParentUploaderUID", "GamePath") + .HasName("pk_chara_data_files"); + + b.HasIndex("FileCacheHash") + .HasDatabaseName("ix_chara_data_files_file_cache_hash"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_files_parent_id"); + + b.ToTable("chara_data_files", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataFileSwap", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("GamePath") + .HasColumnType("text") + .HasColumnName("game_path"); + + b.Property("FilePath") + .HasColumnType("text") + .HasColumnName("file_path"); + + b.HasKey("ParentId", "ParentUploaderUID", "GamePath") + .HasName("pk_chara_data_file_swaps"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_file_swaps_parent_id"); + + b.ToTable("chara_data_file_swaps", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataOriginalFile", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("GamePath") + .HasColumnType("text") + .HasColumnName("game_path"); + + b.Property("Hash") + .HasColumnType("text") + .HasColumnName("hash"); + + b.HasKey("ParentId", "ParentUploaderUID", "GamePath") + .HasName("pk_chara_data_orig_files"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_orig_files_parent_id"); + + b.ToTable("chara_data_orig_files", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataPose", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("PoseData") + .HasColumnType("text") + .HasColumnName("pose_data"); + + b.Property("WorldData") + .HasColumnType("text") + .HasColumnName("world_data"); + + b.HasKey("ParentId", "ParentUploaderUID", "Id") + .HasName("pk_chara_data_poses"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_poses_parent_id"); + + b.ToTable("chara_data_poses", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("RawSize") + .HasColumnType("bigint") + .HasColumnName("raw_size"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("UploadDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("upload_date"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.Property("PreferDisableAnimations") + .HasColumnType("boolean") + .HasColumnName("prefer_disable_animations"); + + b.Property("PreferDisableSounds") + .HasColumnType("boolean") + .HasColumnName("prefer_disable_sounds"); + + b.Property("PreferDisableVFX") + .HasColumnType("boolean") + .HasColumnName("prefer_disable_vfx"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("BannedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_user_uid"); + + b.Property("BannedByUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_by_uid"); + + b.Property("BannedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("banned_on"); + + b.Property("BannedReason") + .HasColumnType("text") + .HasColumnName("banned_reason"); + + b.HasKey("GroupGID", "BannedUserUID") + .HasName("pk_group_bans"); + + b.HasIndex("BannedByUID") + .HasDatabaseName("ix_group_bans_banned_by_uid"); + + b.HasIndex("BannedUserUID") + .HasDatabaseName("ix_group_bans_banned_user_uid"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_bans_group_gid"); + + b.ToTable("group_bans", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPairPreferredPermission", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("DisableVFX") + .HasColumnType("boolean") + .HasColumnName("disable_vfx"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.HasKey("UserUID", "GroupGID") + .HasName("pk_group_pair_preferred_permissions"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pair_preferred_permissions_group_gid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_group_pair_preferred_permissions_user_uid"); + + b.ToTable("group_pair_preferred_permissions", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Invite") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("invite"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.HasKey("GroupGID", "Invite") + .HasName("pk_group_temp_invites"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_temp_invites_group_gid"); + + b.HasIndex("Invite") + .HasDatabaseName("ix_group_temp_invites_invite"); + + b.ToTable("group_temp_invites", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserDefaultPreferredPermission", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("DisableGroupAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_group_animations"); + + b.Property("DisableGroupSounds") + .HasColumnType("boolean") + .HasColumnName("disable_group_sounds"); + + b.Property("DisableGroupVFX") + .HasColumnType("boolean") + .HasColumnName("disable_group_vfx"); + + b.Property("DisableIndividualAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_individual_animations"); + + b.Property("DisableIndividualSounds") + .HasColumnType("boolean") + .HasColumnName("disable_individual_sounds"); + + b.Property("DisableIndividualVFX") + .HasColumnType("boolean") + .HasColumnName("disable_individual_vfx"); + + b.Property("IndividualIsSticky") + .HasColumnType("boolean") + .HasColumnName("individual_is_sticky"); + + b.HasKey("UserUID") + .HasName("pk_user_default_preferred_permissions"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_user_default_preferred_permissions_user_uid"); + + b.ToTable("user_default_preferred_permissions", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserPermissionSet", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("DisableVFX") + .HasColumnType("boolean") + .HasColumnName("disable_vfx"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Sticky") + .HasColumnType("boolean") + .HasColumnName("sticky"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_user_permission_sets"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_user_permission_sets_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_user_permission_sets_user_uid"); + + b.HasIndex("UserUID", "OtherUserUID", "IsPaused") + .HasDatabaseName("ix_user_permission_sets_user_uid_other_user_uid_is_paused"); + + b.ToTable("user_permission_sets", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileData", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("Base64ProfileImage") + .HasColumnType("text") + .HasColumnName("base64profile_image"); + + b.Property("FlaggedForReport") + .HasColumnType("boolean") + .HasColumnName("flagged_for_report"); + + b.Property("IsNSFW") + .HasColumnType("boolean") + .HasColumnName("is_nsfw"); + + b.Property("ProfileDisabled") + .HasColumnType("boolean") + .HasColumnName("profile_disabled"); + + b.Property("UserDescription") + .HasColumnType("text") + .HasColumnName("user_description"); + + b.HasKey("UserUID") + .HasName("pk_user_profile_data"); + + b.ToTable("user_profile_data", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_uid"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_uid"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaData", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataAllowance", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "AllowedGroup") + .WithMany() + .HasForeignKey("AllowedGroupGID") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_allowance_groups_allowed_group_gid"); + + b.HasOne("MareSynchronosShared.Models.User", "AllowedUser") + .WithMany() + .HasForeignKey("AllowedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_allowance_users_allowed_user_uid"); + + b.HasOne("MareSynchronosShared.Models.CharaData", "Parent") + .WithMany("AllowedIndividiuals") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_allowance_chara_data_parent_id_parent_uploader_u"); + + b.Navigation("AllowedGroup"); + + b.Navigation("AllowedUser"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataFile", b => + { + b.HasOne("MareSynchronosShared.Models.FileCache", "FileCache") + .WithMany() + .HasForeignKey("FileCacheHash") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_files_files_file_cache_hash"); + + b.HasOne("MareSynchronosShared.Models.CharaData", "Parent") + .WithMany("Files") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_files_chara_data_parent_id_parent_uploader_uid"); + + b.Navigation("FileCache"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataFileSwap", b => + { + b.HasOne("MareSynchronosShared.Models.CharaData", "Parent") + .WithMany("FileSwaps") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_file_swaps_chara_data_parent_id_parent_uploader_"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataOriginalFile", b => + { + b.HasOne("MareSynchronosShared.Models.CharaData", "Parent") + .WithMany("OriginalFiles") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_orig_files_chara_data_parent_id_parent_uploader_"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataPose", b => + { + b.HasOne("MareSynchronosShared.Models.CharaData", "Parent") + .WithMany("Poses") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_poses_chara_data_parent_id_parent_uploader_uid"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_uid"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_uid"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_uid"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.HasOne("MareSynchronosShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_uid"); + + b.HasOne("MareSynchronosShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_uid"); + + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_groups_group_gid"); + + b.Navigation("BannedBy"); + + b.Navigation("BannedUser"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_gid"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_uid"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPairPreferredPermission", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_groups_group_gid"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_users_user_uid"); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserDefaultPreferredPermission", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_default_preferred_permissions_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserPermissionSet", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_permission_sets_users_other_user_uid"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_permission_sets_users_user_uid"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileData", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaData", b => + { + b.Navigation("AllowedIndividiuals"); + + b.Navigation("FileSwaps"); + + b.Navigation("Files"); + + b.Navigation("OriginalFiles"); + + b.Navigation("Poses"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20250627204223_AllowedGroup.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20250627204223_AllowedGroup.cs new file mode 100644 index 0000000..dd1a979 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20250627204223_AllowedGroup.cs @@ -0,0 +1,98 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + /// + public partial class AllowedGroup : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropPrimaryKey( + name: "pk_chara_data_allowance", + table: "chara_data_allowance"); + + migrationBuilder.AlterColumn( + name: "allowed_user_uid", + table: "chara_data_allowance", + type: "character varying(10)", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(10)"); + + migrationBuilder.AddColumn( + name: "id", + table: "chara_data_allowance", + type: "bigint", + nullable: false, + defaultValue: 0L) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + migrationBuilder.AddColumn( + name: "allowed_group_gid", + table: "chara_data_allowance", + type: "character varying(20)", + nullable: true); + + migrationBuilder.AddPrimaryKey( + name: "pk_chara_data_allowance", + table: "chara_data_allowance", + columns: new[] { "parent_id", "parent_uploader_uid", "id" }); + + migrationBuilder.CreateIndex( + name: "ix_chara_data_allowance_allowed_group_gid", + table: "chara_data_allowance", + column: "allowed_group_gid"); + + migrationBuilder.AddForeignKey( + name: "fk_chara_data_allowance_groups_allowed_group_gid", + table: "chara_data_allowance", + column: "allowed_group_gid", + principalTable: "groups", + principalColumn: "gid", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_chara_data_allowance_groups_allowed_group_gid", + table: "chara_data_allowance"); + + migrationBuilder.DropPrimaryKey( + name: "pk_chara_data_allowance", + table: "chara_data_allowance"); + + migrationBuilder.DropIndex( + name: "ix_chara_data_allowance_allowed_group_gid", + table: "chara_data_allowance"); + + migrationBuilder.DropColumn( + name: "id", + table: "chara_data_allowance"); + + migrationBuilder.DropColumn( + name: "allowed_group_gid", + table: "chara_data_allowance"); + + migrationBuilder.AlterColumn( + name: "allowed_user_uid", + table: "chara_data_allowance", + type: "character varying(10)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "character varying(10)", + oldNullable: true); + + migrationBuilder.AddPrimaryKey( + name: "pk_chara_data_allowance", + table: "chara_data_allowance", + columns: new[] { "parent_id", "parent_uploader_uid", "allowed_user_uid" }); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/MareDbContextModelSnapshot.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/MareDbContextModelSnapshot.cs new file mode 100644 index 0000000..7f857eb --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/MareDbContextModelSnapshot.cs @@ -0,0 +1,992 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + partial class MareDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("IsBanned") + .HasColumnType("boolean") + .HasColumnName("is_banned"); + + b.Property("PrimaryUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("primary_user_uid"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("PrimaryUserUID") + .HasDatabaseName("ix_auth_primary_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaData", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("UploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.Property("AccessType") + .HasColumnType("integer") + .HasColumnName("access_type"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_date"); + + b.Property("CustomizeData") + .HasColumnType("text") + .HasColumnName("customize_data"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("DownloadCount") + .HasColumnType("integer") + .HasColumnName("download_count"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property("GlamourerData") + .HasColumnType("text") + .HasColumnName("glamourer_data"); + + b.Property("ManipulationData") + .HasColumnType("text") + .HasColumnName("manipulation_data"); + + b.Property("ShareType") + .HasColumnType("integer") + .HasColumnName("share_type"); + + b.Property("UpdatedDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_date"); + + b.HasKey("Id", "UploaderUID") + .HasName("pk_chara_data"); + + b.HasIndex("Id") + .HasDatabaseName("ix_chara_data_id"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_chara_data_uploader_uid"); + + b.ToTable("chara_data", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataAllowance", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowedGroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("allowed_group_gid"); + + b.Property("AllowedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("allowed_user_uid"); + + b.HasKey("ParentId", "ParentUploaderUID", "Id") + .HasName("pk_chara_data_allowance"); + + b.HasIndex("AllowedGroupGID") + .HasDatabaseName("ix_chara_data_allowance_allowed_group_gid"); + + b.HasIndex("AllowedUserUID") + .HasDatabaseName("ix_chara_data_allowance_allowed_user_uid"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_allowance_parent_id"); + + b.ToTable("chara_data_allowance", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataFile", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("GamePath") + .HasColumnType("text") + .HasColumnName("game_path"); + + b.Property("FileCacheHash") + .HasColumnType("character varying(40)") + .HasColumnName("file_cache_hash"); + + b.HasKey("ParentId", "ParentUploaderUID", "GamePath") + .HasName("pk_chara_data_files"); + + b.HasIndex("FileCacheHash") + .HasDatabaseName("ix_chara_data_files_file_cache_hash"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_files_parent_id"); + + b.ToTable("chara_data_files", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataFileSwap", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("GamePath") + .HasColumnType("text") + .HasColumnName("game_path"); + + b.Property("FilePath") + .HasColumnType("text") + .HasColumnName("file_path"); + + b.HasKey("ParentId", "ParentUploaderUID", "GamePath") + .HasName("pk_chara_data_file_swaps"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_file_swaps_parent_id"); + + b.ToTable("chara_data_file_swaps", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataOriginalFile", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("GamePath") + .HasColumnType("text") + .HasColumnName("game_path"); + + b.Property("Hash") + .HasColumnType("text") + .HasColumnName("hash"); + + b.HasKey("ParentId", "ParentUploaderUID", "GamePath") + .HasName("pk_chara_data_orig_files"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_orig_files_parent_id"); + + b.ToTable("chara_data_orig_files", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataPose", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("PoseData") + .HasColumnType("text") + .HasColumnName("pose_data"); + + b.Property("WorldData") + .HasColumnType("text") + .HasColumnName("world_data"); + + b.HasKey("ParentId", "ParentUploaderUID", "Id") + .HasName("pk_chara_data_poses"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_poses_parent_id"); + + b.ToTable("chara_data_poses", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("DisableVFX") + .HasColumnType("boolean") + .HasColumnName("disable_vfx"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("UploadDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("upload_date"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("DisableVFX") + .HasColumnType("boolean") + .HasColumnName("disable_vfx"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("BannedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_user_uid"); + + b.Property("BannedByUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_by_uid"); + + b.Property("BannedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("banned_on"); + + b.Property("BannedReason") + .HasColumnType("text") + .HasColumnName("banned_reason"); + + b.HasKey("GroupGID", "BannedUserUID") + .HasName("pk_group_bans"); + + b.HasIndex("BannedByUID") + .HasDatabaseName("ix_group_bans_banned_by_uid"); + + b.HasIndex("BannedUserUID") + .HasDatabaseName("ix_group_bans_banned_user_uid"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_bans_group_gid"); + + b.ToTable("group_bans", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("DisableVFX") + .HasColumnType("boolean") + .HasColumnName("disable_vfx"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Invite") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("invite"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.HasKey("GroupGID", "Invite") + .HasName("pk_group_temp_invites"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_temp_invites_group_gid"); + + b.HasIndex("Invite") + .HasDatabaseName("ix_group_temp_invites_invite"); + + b.ToTable("group_temp_invites", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileData", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("Base64ProfileImage") + .HasColumnType("text") + .HasColumnName("base64profile_image"); + + b.Property("FlaggedForReport") + .HasColumnType("boolean") + .HasColumnName("flagged_for_report"); + + b.Property("IsNSFW") + .HasColumnType("boolean") + .HasColumnName("is_nsfw"); + + b.Property("ProfileDisabled") + .HasColumnType("boolean") + .HasColumnName("profile_disabled"); + + b.Property("UserDescription") + .HasColumnType("text") + .HasColumnName("user_description"); + + b.HasKey("UserUID") + .HasName("pk_user_profile_data"); + + b.ToTable("user_profile_data", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileDataReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ReportDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("report_date"); + + b.Property("ReportReason") + .HasColumnType("text") + .HasColumnName("report_reason"); + + b.Property("ReportedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("reported_user_uid"); + + b.Property("ReportingUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("reporting_user_uid"); + + b.HasKey("Id") + .HasName("pk_user_profile_data_reports"); + + b.HasIndex("ReportedUserUID") + .HasDatabaseName("ix_user_profile_data_reports_reported_user_uid"); + + b.HasIndex("ReportingUserUID") + .HasDatabaseName("ix_user_profile_data_reports_reporting_user_uid"); + + b.ToTable("user_profile_data_reports", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_uid"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_uid"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaData", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataAllowance", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "AllowedGroup") + .WithMany() + .HasForeignKey("AllowedGroupGID") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_allowance_groups_allowed_group_gid"); + + b.HasOne("MareSynchronosShared.Models.User", "AllowedUser") + .WithMany() + .HasForeignKey("AllowedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_allowance_users_allowed_user_uid"); + + b.HasOne("MareSynchronosShared.Models.CharaData", "Parent") + .WithMany("AllowedIndividiuals") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_allowance_chara_data_parent_id_parent_uploader_u"); + + b.Navigation("AllowedGroup"); + + b.Navigation("AllowedUser"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataFile", b => + { + b.HasOne("MareSynchronosShared.Models.FileCache", "FileCache") + .WithMany() + .HasForeignKey("FileCacheHash") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_files_files_file_cache_hash"); + + b.HasOne("MareSynchronosShared.Models.CharaData", "Parent") + .WithMany("Files") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_files_chara_data_parent_id_parent_uploader_uid"); + + b.Navigation("FileCache"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataFileSwap", b => + { + b.HasOne("MareSynchronosShared.Models.CharaData", "Parent") + .WithMany("FileSwaps") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_file_swaps_chara_data_parent_id_parent_uploader_"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataOriginalFile", b => + { + b.HasOne("MareSynchronosShared.Models.CharaData", "Parent") + .WithMany("OriginalFiles") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_orig_files_chara_data_parent_id_parent_uploader_"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaDataPose", b => + { + b.HasOne("MareSynchronosShared.Models.CharaData", "Parent") + .WithMany("Poses") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_poses_chara_data_parent_id_parent_uploader_uid"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_uid"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_uid"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_uid"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.HasOne("MareSynchronosShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_uid"); + + b.HasOne("MareSynchronosShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_uid"); + + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_groups_group_gid"); + + b.Navigation("BannedBy"); + + b.Navigation("BannedUser"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_gid"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_uid"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileData", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileDataReport", b => + { + b.HasOne("MareSynchronosShared.Models.User", "ReportedUser") + .WithMany() + .HasForeignKey("ReportedUserUID") + .HasConstraintName("fk_user_profile_data_reports_users_reported_user_uid"); + + b.HasOne("MareSynchronosShared.Models.User", "ReportingUser") + .WithMany() + .HasForeignKey("ReportingUserUID") + .HasConstraintName("fk_user_profile_data_reports_users_reporting_user_uid"); + + b.Navigation("ReportedUser"); + + b.Navigation("ReportingUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.CharaData", b => + { + b.Navigation("AllowedIndividiuals"); + + b.Navigation("FileSwaps"); + + b.Navigation("Files"); + + b.Navigation("OriginalFiles"); + + b.Navigation("Poses"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Models/Auth.cs b/MareSynchronosServer/MareSynchronosShared/Models/Auth.cs new file mode 100644 index 0000000..49113aa --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Models/Auth.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace MareSynchronosShared.Models; + +public class Auth +{ + [Key] + [MaxLength(64)] + public string HashedKey { get; set; } + + public string UserUID { get; set; } + public User User { get; set; } + public bool IsBanned { get; set; } + public string? PrimaryUserUID { get; set; } + public User? PrimaryUser { get; set; } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Models/Banned.cs b/MareSynchronosServer/MareSynchronosShared/Models/Banned.cs new file mode 100644 index 0000000..c38e11d --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Models/Banned.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace MareSynchronosShared.Models; + +public class Banned +{ + [Key] + [MaxLength(100)] + public string CharacterIdentification { get; set; } + public string Reason { get; set; } + [Timestamp] + public byte[] Timestamp { get; set; } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Models/BannedRegistrations.cs b/MareSynchronosServer/MareSynchronosShared/Models/BannedRegistrations.cs new file mode 100644 index 0000000..70d2ffe --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Models/BannedRegistrations.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace MareSynchronosShared.Models; + +public class BannedRegistrations +{ + [Key] + [MaxLength(100)] + public string DiscordIdOrLodestoneAuth { get; set; } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Models/CharaData.cs b/MareSynchronosServer/MareSynchronosShared/Models/CharaData.cs new file mode 100644 index 0000000..41fe744 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Models/CharaData.cs @@ -0,0 +1,91 @@ +using System.ComponentModel.DataAnnotations; + +namespace MareSynchronosShared.Models; + +public enum CharaDataAccess +{ + Individuals, + ClosePairs, + AllPairs, + Public +} + +public enum CharaDataShare +{ + Private, + Shared +} + +public class CharaData +{ + public string Id { get; set; } + public virtual User Uploader { get; set; } + public string UploaderUID { get; set; } + public DateTime CreatedDate { get; set; } + public DateTime UpdatedDate { get; set; } + public string Description { get; set; } + public CharaDataAccess AccessType { get; set; } + public CharaDataShare ShareType { get; set; } + public DateTime? ExpiryDate { get; set; } + public string? GlamourerData { get; set; } + public string? CustomizeData { get; set; } + public string? ManipulationData { get; set; } + public int DownloadCount { get; set; } = 0; + public virtual ICollection Poses { get; set; } = []; + public virtual ICollection Files { get; set; } = []; + public virtual ICollection FileSwaps { get; set; } = []; + public virtual ICollection OriginalFiles { get; set; } = []; + public virtual ICollection AllowedIndividiuals { get; set; } = []; +} + +public class CharaDataAllowance +{ + [Key] + public long Id { get; set; } + public virtual CharaData Parent { get; set; } + public string ParentId { get; set; } + public string ParentUploaderUID { get; set; } + public virtual User? AllowedUser { get; set; } + public string? AllowedUserUID { get; set; } + public virtual Group? AllowedGroup { get; set; } + public string? AllowedGroupGID { get; set; } +} + +public class CharaDataOriginalFile +{ + public virtual CharaData Parent { get; set; } + public string ParentId { get; set; } + public string ParentUploaderUID { get; set; } + public string GamePath { get; set; } + public string Hash { get; set; } +} + +public class CharaDataFile +{ + public virtual FileCache FileCache { get; set; } + public string FileCacheHash { get; set; } + public string GamePath { get; set; } + public virtual CharaData Parent { get; set; } + public string ParentId { get; set; } + public string ParentUploaderUID { get; set; } +} + +public class CharaDataFileSwap +{ + public virtual CharaData Parent { get; set; } + public string ParentId { get; set; } + public string ParentUploaderUID { get; set; } + public string GamePath { get; set; } + public string FilePath { get; set; } +} + +public class CharaDataPose +{ + public long Id { get; set; } + public virtual CharaData Parent { get; set; } + public string ParentId { get; set; } + public string ParentUploaderUID { get; set; } + public string Description { get; set; } + public string PoseData { get; set; } + public string WorldData { get; set; } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Models/ClientPair.cs b/MareSynchronosServer/MareSynchronosShared/Models/ClientPair.cs new file mode 100644 index 0000000..694ccdc --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Models/ClientPair.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace MareSynchronosShared.Models; + +public class ClientPair +{ + [MaxLength(10)] + public string UserUID { get; set; } + public User User { get; set; } + [MaxLength(10)] + public string OtherUserUID { get; set; } + public User OtherUser { get; set; } + public bool IsPaused { get; set; } + public bool AllowReceivingMessages { get; set; } = false; + [Timestamp] + public byte[] Timestamp { get; set; } + public bool DisableSounds { get; set; } = false; + public bool DisableAnimations { get; set; } = false; + public bool DisableVFX { get; set; } = false; +} diff --git a/MareSynchronosServer/MareSynchronosShared/Models/FileCache.cs b/MareSynchronosServer/MareSynchronosShared/Models/FileCache.cs new file mode 100644 index 0000000..3ffd2c2 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Models/FileCache.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace MareSynchronosShared.Models; + +public class FileCache +{ + [Key] + [MaxLength(40)] + public string Hash { get; set; } + [MaxLength(10)] + public string UploaderUID { get; set; } + public User Uploader { get; set; } + public bool Uploaded { get; set; } + public DateTime UploadDate { get; set; } + [Timestamp] + public byte[] Timestamp { get; set; } + public long Size { get; set; } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Models/ForbiddenUploadEntry.cs b/MareSynchronosServer/MareSynchronosShared/Models/ForbiddenUploadEntry.cs new file mode 100644 index 0000000..12c946f --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Models/ForbiddenUploadEntry.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace MareSynchronosShared.Models; + +public class ForbiddenUploadEntry +{ + [Key] + [MaxLength(40)] + public string Hash { get; set; } + [MaxLength(100)] + public string ForbiddenBy { get; set; } + [Timestamp] + public byte[] Timestamp { get; set; } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Models/Group.cs b/MareSynchronosServer/MareSynchronosShared/Models/Group.cs new file mode 100644 index 0000000..72f35d7 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Models/Group.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace MareSynchronosShared.Models; + +public class Group +{ + [Key] + [MaxLength(20)] + public string GID { get; set; } + public string OwnerUID { get; set; } + public User Owner { get; set; } + [MaxLength(50)] + public string Alias { get; set; } + public bool InvitesEnabled { get; set; } + public string HashedPassword { get; set; } + public bool DisableSounds { get; set; } + public bool DisableAnimations { get; set; } + public bool DisableVFX { get; set; } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Models/GroupBan.cs b/MareSynchronosServer/MareSynchronosShared/Models/GroupBan.cs new file mode 100644 index 0000000..ad92530 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Models/GroupBan.cs @@ -0,0 +1,13 @@ +namespace MareSynchronosShared.Models; + +public class GroupBan +{ + public Group Group { get; set; } + public string GroupGID { get; set; } + public User BannedUser { get; set; } + public string BannedUserUID { get; set; } + public User BannedBy { get; set; } + public string BannedByUID { get; set; } + public DateTime BannedOn { get; set; } + public string BannedReason { get; set; } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Models/GroupPair.cs b/MareSynchronosServer/MareSynchronosShared/Models/GroupPair.cs new file mode 100644 index 0000000..57abe80 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Models/GroupPair.cs @@ -0,0 +1,15 @@ +namespace MareSynchronosShared.Models; + +public class GroupPair +{ + public string GroupGID { get; set; } + public Group Group { get; set; } + public string GroupUserUID { get; set; } + public User GroupUser { get; set; } + public bool IsPaused { get; set; } + public bool IsPinned { get; set; } + public bool IsModerator { get; set; } + public bool DisableSounds { get; set; } + public bool DisableAnimations { get; set; } + public bool DisableVFX { get; set; } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Models/GroupTempInvite.cs b/MareSynchronosServer/MareSynchronosShared/Models/GroupTempInvite.cs new file mode 100644 index 0000000..d3f81b7 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Models/GroupTempInvite.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace MareSynchronosShared.Models; + +public class GroupTempInvite +{ + public Group Group { get; set; } + public string GroupGID { get; set; } + [MaxLength(64)] + public string Invite { get; set; } + public DateTime ExpirationDate { get; set; } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Models/LodeStoneAuth.cs b/MareSynchronosServer/MareSynchronosShared/Models/LodeStoneAuth.cs new file mode 100644 index 0000000..30afd19 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Models/LodeStoneAuth.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace MareSynchronosShared.Models; + +public class LodeStoneAuth +{ + [Key] + public ulong DiscordId { get; set; } + [MaxLength(100)] + public string HashedLodestoneId { get; set; } + [MaxLength(100)] + public string? LodestoneAuthString { get; set; } + public User? User { get; set; } + public DateTime? StartedAt { get; set; } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Models/User.cs b/MareSynchronosServer/MareSynchronosShared/Models/User.cs new file mode 100644 index 0000000..6fcb2f6 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Models/User.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace MareSynchronosShared.Models; + +public class User +{ + [Key] + [MaxLength(10)] + public string UID { get; set; } + [Timestamp] + public byte[] Timestamp { get; set; } + + public bool IsModerator { get; set; } = false; + + public bool IsAdmin { get; set; } = false; + + public DateTime LastLoggedIn { get; set; } + [MaxLength(15)] + public string Alias { get; set; } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Models/UserProfileData.cs b/MareSynchronosServer/MareSynchronosShared/Models/UserProfileData.cs new file mode 100644 index 0000000..9d8e848 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Models/UserProfileData.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace MareSynchronosShared.Models; + +public class UserProfileData +{ + public string Base64ProfileImage { get; set; } + public bool FlaggedForReport { get; set; } + public bool IsNSFW { get; set; } + public bool ProfileDisabled { get; set; } + public User User { get; set; } + + public string UserDescription { get; set; } + + [Required] + [Key] + [ForeignKey(nameof(User))] + public string UserUID { get; set; } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Models/UserProfileDataReport.cs b/MareSynchronosServer/MareSynchronosShared/Models/UserProfileDataReport.cs new file mode 100644 index 0000000..cd53b76 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Models/UserProfileDataReport.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace MareSynchronosShared.Models; + +public class UserProfileDataReport +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + public DateTime ReportDate { get; set; } + public User ReportedUser { get; set; } + + [ForeignKey(nameof(ReportedUser))] + public string ReportedUserUID { get; set; } + + public User ReportingUser { get; set; } + + [ForeignKey(nameof(ReportingUser))] + public string ReportingUserUID { get; set; } + + public string ReportReason { get; set; } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/RequirementHandlers/UserRequirement.cs b/MareSynchronosServer/MareSynchronosShared/RequirementHandlers/UserRequirement.cs new file mode 100644 index 0000000..317a30c --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/RequirementHandlers/UserRequirement.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Authorization; + +namespace MareSynchronosShared.RequirementHandlers; + +public class UserRequirement : IAuthorizationRequirement +{ + public UserRequirement(UserRequirements requirements) + { + Requirements = requirements; + } + + public UserRequirements Requirements { get; } +} diff --git a/MareSynchronosServer/MareSynchronosShared/RequirementHandlers/UserRequirementHandler.cs b/MareSynchronosServer/MareSynchronosShared/RequirementHandlers/UserRequirementHandler.cs new file mode 100644 index 0000000..5cc38e5 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/RequirementHandlers/UserRequirementHandler.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using MareSynchronosShared.Utils; +using StackExchange.Redis; +using StackExchange.Redis.Extensions.Core.Abstractions; +using Microsoft.Extensions.Logging; + +namespace MareSynchronosShared.RequirementHandlers; + +public class UserRequirementHandler : AuthorizationHandler +{ + private readonly IDbContextFactory _dbContextFactory; + private readonly ILogger _logger; + private readonly IRedisDatabase _redis; + + public UserRequirementHandler(IDbContextFactory dbContextFactory, ILogger logger, IRedisDatabase redisDb) + { + _dbContextFactory = dbContextFactory; + _logger = logger; + _redis = redisDb; + } + + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, UserRequirement requirement, HubInvocationContext resource) + { + var uid = context.User.Claims.SingleOrDefault(g => string.Equals(g.Type, MareClaimTypes.Uid, StringComparison.Ordinal))?.Value; + + if (uid == null) context.Fail(); + + if ((requirement.Requirements & UserRequirements.Identified) is UserRequirements.Identified) + { + var ident = await _redis.GetAsync("UID:" + uid).ConfigureAwait(false); + if (ident == RedisValue.EmptyString) context.Fail(); + } + + if ((requirement.Requirements & UserRequirements.Administrator) is UserRequirements.Administrator) + { + using var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false); + var user = await dbContext.Users.AsNoTracking().SingleOrDefaultAsync(b => b.UID == uid).ConfigureAwait(false); + if (user == null || !user.IsAdmin) context.Fail(); + _logger.LogInformation("Admin {uid} authenticated", uid); + } + + if ((requirement.Requirements & UserRequirements.Moderator) is UserRequirements.Moderator) + { + using var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false); + var user = await dbContext.Users.AsNoTracking().SingleOrDefaultAsync(b => b.UID == uid).ConfigureAwait(false); + if (user == null || !user.IsAdmin && !user.IsModerator) context.Fail(); + _logger.LogInformation("Admin/Moderator {uid} authenticated", uid); + } + + context.Succeed(requirement); + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/RequirementHandlers/UserRequirements.cs b/MareSynchronosServer/MareSynchronosShared/RequirementHandlers/UserRequirements.cs new file mode 100644 index 0000000..95fed1f --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/RequirementHandlers/UserRequirements.cs @@ -0,0 +1,8 @@ +namespace MareSynchronosShared.RequirementHandlers; + +public enum UserRequirements +{ + Identified = 0b00000001, + Moderator = 0b00000010, + Administrator = 0b00000100, +} diff --git a/MareSynchronosServer/MareSynchronosShared/Services/IConfigurationService.cs b/MareSynchronosServer/MareSynchronosShared/Services/IConfigurationService.cs new file mode 100644 index 0000000..85c21e6 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Services/IConfigurationService.cs @@ -0,0 +1,11 @@ +using MareSynchronosShared.Utils.Configuration; + +namespace MareSynchronosShared.Services; + +public interface IConfigurationService where T : class, IMareConfiguration +{ + bool IsMain { get; } + T1 GetValue(string key); + T1 GetValueOrDefault(string key, T1 defaultValue); + string ToString(); +} diff --git a/MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationController.cs b/MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationController.cs new file mode 100644 index 0000000..4b9a01d --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationController.cs @@ -0,0 +1,60 @@ +using MareSynchronosShared.Utils.Configuration; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace MareSynchronosShared.Services; + +[Route("configuration/[controller]")] +[Authorize(Policy = "Internal")] +public class MareConfigurationController : Controller where T : class, IMareConfiguration +{ + private readonly ILogger> _logger; + private IOptionsMonitor _config; + + public MareConfigurationController(IOptionsMonitor config, ILogger> logger) + { + _config = config; + _logger = logger; + } + + [HttpGet("GetConfigurationEntry")] + [Authorize(Policy = "Internal")] + public IActionResult GetConfigurationEntry(string key, string defaultValue) + { + var result = _config.CurrentValue.SerializeValue(key, defaultValue); + _logger.LogInformation("Requested " + key + ", returning:" + result); + return Ok(result); + } +} + +#pragma warning disable MA0048 // File name must match type name +public class MareStaticFilesServerConfigurationController : MareConfigurationController +{ + public MareStaticFilesServerConfigurationController(IOptionsMonitor config, ILogger logger) : base(config, logger) + { + } +} + +public class MareBaseConfigurationController : MareConfigurationController +{ + public MareBaseConfigurationController(IOptionsMonitor config, ILogger logger) : base(config, logger) + { + } +} + +public class MareServerConfigurationController : MareConfigurationController +{ + public MareServerConfigurationController(IOptionsMonitor config, ILogger logger) : base(config, logger) + { + } +} + +public class MareServicesConfigurationController : MareConfigurationController +{ + public MareServicesConfigurationController(IOptionsMonitor config, ILogger logger) : base(config, logger) + { + } +} +#pragma warning restore MA0048 // File name must match type name diff --git a/MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationServiceClient.cs b/MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationServiceClient.cs new file mode 100644 index 0000000..6eaff9f --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationServiceClient.cs @@ -0,0 +1,189 @@ +using MareSynchronosShared.Utils; +using MareSynchronosShared.Utils.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Collections; +using System.Collections.Concurrent; +using System.Globalization; +using System.Net.Http.Headers; +using System.Reflection; +using System.Text; +using System.Text.Json; + +namespace MareSynchronosShared.Services; + +public class MareConfigurationServiceClient : IHostedService, IConfigurationService where T : class, IMareConfiguration +{ + private readonly IOptionsMonitor _config; + private readonly ConcurrentDictionary _cachedRemoteProperties = new(StringComparer.Ordinal); + private readonly ILogger> _logger; + private readonly ServerTokenGenerator _serverTokenGenerator; + private readonly CancellationTokenSource _updateTaskCts = new(); + private bool _initialized = false; + private readonly HttpClient _httpClient; + + private Uri GetRoute(string key, string value) + { + if (_config.CurrentValue.GetType() == typeof(ServerConfiguration)) + return new Uri((_config.CurrentValue as ServerConfiguration).MainServerAddress, $"configuration/MareServerConfiguration/{nameof(MareServerConfigurationController.GetConfigurationEntry)}?key={key}&defaultValue={value}"); + if (_config.CurrentValue.GetType() == typeof(MareConfigurationBase)) + return new Uri((_config.CurrentValue as MareConfigurationBase).MainServerAddress, $"configuration/MareBaseConfiguration/{nameof(MareBaseConfigurationController.GetConfigurationEntry)}?key={key}&defaultValue={value}"); + if (_config.CurrentValue.GetType() == typeof(ServicesConfiguration)) + return new Uri((_config.CurrentValue as ServicesConfiguration).MainServerAddress, $"configuration/MareServicesConfiguration/{nameof(MareServicesConfigurationController.GetConfigurationEntry)}?key={key}&defaultValue={value}"); + if (_config.CurrentValue.GetType() == typeof(StaticFilesServerConfiguration)) + return new Uri((_config.CurrentValue as StaticFilesServerConfiguration).MainFileServerAddress, $"configuration/MareStaticFilesServerConfiguration/{nameof(MareStaticFilesServerConfigurationController.GetConfigurationEntry)}?key={key}&defaultValue={value}"); + + throw new NotSupportedException("Config is not supported to be gotten remotely"); + } + + public MareConfigurationServiceClient(ILogger> logger, IOptionsMonitor config, ServerTokenGenerator serverTokenGenerator) + { + _config = config; + _logger = logger; + _serverTokenGenerator = serverTokenGenerator; + _httpClient = new(); + } + + public bool IsMain => false; + + public T1 GetValueOrDefault(string key, T1 defaultValue) + { + var prop = _config.CurrentValue.GetType().GetProperty(key); + if (prop == null) return defaultValue; + if (prop.PropertyType != typeof(T1)) throw new InvalidCastException($"Invalid Cast: Property {key} is {prop.PropertyType}, wanted: {typeof(T1)}"); + bool isRemote = prop.GetCustomAttributes(typeof(RemoteConfigurationAttribute), inherit: true).Any(); + if (isRemote && _cachedRemoteProperties.TryGetValue(key, out var remotevalue)) + { + return (T1)remotevalue; + } + + var value = prop.GetValue(_config.CurrentValue); + var defaultPropValue = prop.PropertyType.IsValueType ? Activator.CreateInstance(prop.PropertyType) : null; + if (value == defaultPropValue) return defaultValue; + return (T1)value; + } + + public T1 GetValue(string key) + { + var prop = _config.CurrentValue.GetType().GetProperty(key); + if (prop == null) throw new KeyNotFoundException(key); + if (prop.PropertyType != typeof(T1)) throw new InvalidCastException($"Invalid Cast: Property {key} is {prop.PropertyType}, wanted: {typeof(T1)}"); + bool isRemote = prop.GetCustomAttributes(typeof(RemoteConfigurationAttribute), inherit: true).Any(); + if (isRemote && _cachedRemoteProperties.TryGetValue(key, out var remotevalue)) + { + return (T1)remotevalue; + } + + var value = prop.GetValue(_config.CurrentValue); + return (T1)value; + } + + public override string ToString() + { + var props = _config.CurrentValue.GetType().GetProperties(); + StringBuilder sb = new(); + foreach (var prop in props) + { + var isRemote = prop.GetCustomAttributes(typeof(RemoteConfigurationAttribute), true).Any(); + var getValueMethod = GetType().GetMethod(nameof(GetValue)).MakeGenericMethod(prop.PropertyType); + var value = isRemote ? getValueMethod.Invoke(this, new[] { prop.Name }) : prop.GetValue(_config.CurrentValue); + if (typeof(IEnumerable).IsAssignableFrom(prop.PropertyType) && !typeof(string).IsAssignableFrom(prop.PropertyType)) + { + var enumVal = (IEnumerable)value; + value = string.Empty; + foreach (var listVal in enumVal) + { + value += listVal.ToString() + ", "; + } + } + sb.AppendLine($"{prop.Name} (IsRemote: {isRemote}) => {value}"); + } + return sb.ToString(); + } + + private async Task GetValueFromRemote(string key, object defaultValue) + { + try + { + _logger.LogInformation("Getting {key} from Http", key); + using HttpRequestMessage msg = new(HttpMethod.Get, GetRoute(key, Convert.ToString(defaultValue, CultureInfo.InvariantCulture))); + msg.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _serverTokenGenerator.Token); + using var response = await _httpClient.SendAsync(msg).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + _logger.LogInformation("Http Response for {key} = {value}", key, content); + return JsonSerializer.Deserialize(content); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failure Getting Remote Entry for {key}", key); + return (T1)defaultValue; + } + } + + private async Task UpdateRemoteProperties(CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + _logger.LogInformation("Getting Properties from Remote for " + typeof(T)); + try + { + var properties = _config.CurrentValue.GetType().GetProperties(); + foreach (var prop in properties) + { + try + { + if (!prop.GetCustomAttributes(typeof(RemoteConfigurationAttribute), true).Any()) continue; + _logger.LogInformation("Checking Property " + prop.Name); + var mi = GetType().GetMethod(nameof(GetValueFromRemote), BindingFlags.NonPublic | BindingFlags.Instance).MakeGenericMethod(prop.PropertyType); + var defaultValue = prop.PropertyType.IsValueType ? Activator.CreateInstance(prop.PropertyType) : null; + var task = (Task)mi.Invoke(this, new[] { prop.Name, defaultValue }); + await task.ConfigureAwait(false); + + var resultProperty = task.GetType().GetProperty("Result"); + var resultValue = resultProperty.GetValue(task); + + if (resultValue != defaultValue) + { + _cachedRemoteProperties[prop.Name] = resultValue; + _logger.LogInformation(prop.Name + " is now " + resultValue.ToString()); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during getting property " + prop.Name); + } + } + + if (!_initialized) + { + _initialized = true; + } + + _logger.LogInformation("Saved properties from HTTP are now:"); + _logger.LogInformation(ToString()); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failure getting or updating properties from HTTP, retrying in 30min"); + } + + await Task.Delay(TimeSpan.FromMinutes(30), ct).ConfigureAwait(false); + } + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Starting MareConfigurationServiceClient"); + _ = UpdateRemoteProperties(_updateTaskCts.Token); + while (!_initialized && !cancellationToken.IsCancellationRequested) await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _updateTaskCts.Cancel(); + _httpClient.Dispose(); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationServiceServer.cs b/MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationServiceServer.cs new file mode 100644 index 0000000..657f5a4 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationServiceServer.cs @@ -0,0 +1,51 @@ +using MareSynchronosShared.Utils; +using MareSynchronosShared.Utils.Configuration; +using Microsoft.Extensions.Options; +using System.Collections; +using System.Text; + +namespace MareSynchronosShared.Services; + +public class MareConfigurationServiceServer : IConfigurationService where T : class, IMareConfiguration +{ + private readonly IOptionsMonitor _config; + public bool IsMain => true; + + public MareConfigurationServiceServer(IOptionsMonitor config) + { + _config = config; + } + + public T1 GetValueOrDefault(string key, T1 defaultValue) + { + return _config.CurrentValue.GetValueOrDefault(key, defaultValue); + } + + public T1 GetValue(string key) + { + return _config.CurrentValue.GetValue(key); + } + + public override string ToString() + { + var props = _config.CurrentValue.GetType().GetProperties(); + StringBuilder sb = new(); + foreach (var prop in props) + { + var isRemote = prop.GetCustomAttributes(typeof(RemoteConfigurationAttribute), true).Any(); + var getValueMethod = GetType().GetMethod(nameof(GetValue)).MakeGenericMethod(prop.PropertyType); + var value = isRemote ? getValueMethod.Invoke(this, new[] { prop.Name }) : prop.GetValue(_config.CurrentValue); + if (typeof(IEnumerable).IsAssignableFrom(prop.PropertyType) && !typeof(string).IsAssignableFrom(prop.PropertyType)) + { + var enumVal = (IEnumerable)value; + value = string.Empty; + foreach (var listVal in enumVal) + { + value += listVal.ToString() + ", "; + } + } + sb.AppendLine($"{prop.Name} (IsRemote: {isRemote}) => {value}"); + } + return sb.ToString(); + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/AllowedControllersFeatureProvider.cs b/MareSynchronosServer/MareSynchronosShared/Utils/AllowedControllersFeatureProvider.cs new file mode 100644 index 0000000..1001a3b --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Utils/AllowedControllersFeatureProvider.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Mvc.Controllers; +using System.Reflection; +using Microsoft.Extensions.Logging; + +namespace MareSynchronosShared.Utils; + +public class AllowedControllersFeatureProvider : ControllerFeatureProvider +{ + private readonly ILogger _logger; + private readonly Type[] _allowedTypes; + + public AllowedControllersFeatureProvider(params Type[] allowedTypes) + { + _allowedTypes = allowedTypes; + } + + protected override bool IsController(TypeInfo typeInfo) + { + return base.IsController(typeInfo) && _allowedTypes.Contains(typeInfo.AsType()); + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/ClientMessage.cs b/MareSynchronosServer/MareSynchronosShared/Utils/ClientMessage.cs new file mode 100644 index 0000000..cc7e294 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Utils/ClientMessage.cs @@ -0,0 +1,4 @@ +using MareSynchronos.API.Data.Enum; + +namespace MareSynchronosShared.Utils; +public record ClientMessage(MessageSeverity Severity, string Message, string UID); diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/AuthServiceConfiguration.cs b/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/AuthServiceConfiguration.cs new file mode 100644 index 0000000..94385b7 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/AuthServiceConfiguration.cs @@ -0,0 +1,29 @@ +using System.Text; + +namespace MareSynchronosShared.Utils.Configuration; + +public class AuthServiceConfiguration : MareConfigurationBase +{ + public string GeoIPDbCityFile { get; set; } = string.Empty; + public bool UseGeoIP { get; set; } = false; + public int FailedAuthForTempBan { get; set; } = 5; + public int TempBanDurationInMinutes { get; set; } = 5; + public List WhitelistedIps { get; set; } = new(); + + public int RegisterIpLimit { get; set; } = 3; + public int RegisterIpDurationInMinutes { get; set; } = 10; + + public string WellKnown { get; set; } = string.Empty; + + public override string ToString() + { + StringBuilder sb = new(); + sb.AppendLine(base.ToString()); + sb.AppendLine($"{nameof(GeoIPDbCityFile)} => {GeoIPDbCityFile}"); + sb.AppendLine($"{nameof(UseGeoIP)} => {UseGeoIP}"); + sb.AppendLine($"{nameof(RegisterIpLimit)} => {RegisterIpLimit}"); + sb.AppendLine($"{nameof(RegisterIpDurationInMinutes)} => {RegisterIpDurationInMinutes}"); + sb.AppendLine($"{nameof(WellKnown)} => {WellKnown}"); + return sb.ToString(); + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/CdnShardConfiguration.cs b/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/CdnShardConfiguration.cs new file mode 100644 index 0000000..32ab367 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/CdnShardConfiguration.cs @@ -0,0 +1,13 @@ +namespace MareSynchronosShared.Utils.Configuration; + +public class CdnShardConfiguration +{ + public List Continents { get; set; } + public string FileMatch { get; set; } + public Uri CdnFullUrl { get; set; } + + public override string ToString() + { + return CdnFullUrl.ToString() + "[" + string.Join(',', Continents) + "] == " + FileMatch; + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/IMareConfiguration.cs b/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/IMareConfiguration.cs new file mode 100644 index 0000000..e18a839 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/IMareConfiguration.cs @@ -0,0 +1,8 @@ +namespace MareSynchronosShared.Utils.Configuration; + +public interface IMareConfiguration +{ + T GetValueOrDefault(string key, T defaultValue); + T GetValue(string key); + string SerializeValue(string key, string defaultValue); +} diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/MareConfigurationBase.cs b/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/MareConfigurationBase.cs new file mode 100644 index 0000000..0bd2c62 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/MareConfigurationBase.cs @@ -0,0 +1,51 @@ +using System.Reflection; +using System.Text; +using System.Text.Json; + +namespace MareSynchronosShared.Utils.Configuration; + +public class MareConfigurationBase : IMareConfiguration +{ + public int DbContextPoolSize { get; set; } = 100; + public string Jwt { get; set; } = string.Empty; + public Uri MainServerAddress { get; set; } + public int RedisPool { get; set; } = 50; + public int MetricsPort { get; set; } + public string RedisConnectionString { get; set; } = string.Empty; + public string ShardName { get; set; } = string.Empty; + + public T GetValue(string key) + { + var prop = GetType().GetProperty(key); + if (prop == null) throw new KeyNotFoundException(key); + if (prop.PropertyType != typeof(T)) throw new ArgumentException($"Requested {key} with T:{typeof(T)}, where {key} is {prop.PropertyType}"); + return (T)prop.GetValue(this); + } + + public T GetValueOrDefault(string key, T defaultValue) + { + var prop = GetType().GetProperty(key); + if (prop.PropertyType != typeof(T)) throw new ArgumentException($"Requested {key} with T:{typeof(T)}, where {key} is {prop.PropertyType}"); + if (prop == null) return defaultValue; + return (T)prop.GetValue(this); + } + + public string SerializeValue(string key, string defaultValue) + { + var prop = GetType().GetProperty(key); + if (prop == null) return defaultValue; + if (prop.GetCustomAttribute() == null) return defaultValue; + return JsonSerializer.Serialize(prop.GetValue(this), prop.PropertyType); + } + + public override string ToString() + { + StringBuilder sb = new(); + sb.AppendLine(base.ToString()); + sb.AppendLine($"{nameof(MainServerAddress)} => {MainServerAddress}"); + sb.AppendLine($"{nameof(RedisConnectionString)} => {RedisConnectionString}"); + sb.AppendLine($"{nameof(ShardName)} => {ShardName}"); + sb.AppendLine($"{nameof(DbContextPoolSize)} => {DbContextPoolSize}"); + return sb.ToString(); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/ServerConfiguration.cs b/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/ServerConfiguration.cs new file mode 100644 index 0000000..879dbfb --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/ServerConfiguration.cs @@ -0,0 +1,46 @@ +using System.Text; + +namespace MareSynchronosShared.Utils.Configuration; + +public class ServerConfiguration : MareConfigurationBase +{ + [RemoteConfiguration] + public Uri CdnFullUrl { get; set; } = null; + + [RemoteConfiguration] + public Version ExpectedClientVersion { get; set; } = new Version(0, 0, 0); + + [RemoteConfiguration] + public int MaxExistingGroupsByUser { get; set; } = 3; + + [RemoteConfiguration] + public int MaxGroupUserCount { get; set; } = 100; + + [RemoteConfiguration] + public int MaxJoinedGroupsByUser { get; set; } = 6; + + [RemoteConfiguration] + public bool PurgeUnusedAccounts { get; set; } = false; + + [RemoteConfiguration] + public int PurgeUnusedAccountsPeriodInDays { get; set; } = 14; + + [RemoteConfiguration] + public int MaxCharaDataByUser { get; set; } = 10; + + public override string ToString() + { + StringBuilder sb = new(); + sb.AppendLine(base.ToString()); + sb.AppendLine($"{nameof(CdnFullUrl)} => {CdnFullUrl}"); + sb.AppendLine($"{nameof(RedisConnectionString)} => {RedisConnectionString}"); + sb.AppendLine($"{nameof(ExpectedClientVersion)} => {ExpectedClientVersion}"); + sb.AppendLine($"{nameof(MaxExistingGroupsByUser)} => {MaxExistingGroupsByUser}"); + sb.AppendLine($"{nameof(MaxJoinedGroupsByUser)} => {MaxJoinedGroupsByUser}"); + sb.AppendLine($"{nameof(MaxGroupUserCount)} => {MaxGroupUserCount}"); + sb.AppendLine($"{nameof(PurgeUnusedAccounts)} => {PurgeUnusedAccounts}"); + sb.AppendLine($"{nameof(PurgeUnusedAccountsPeriodInDays)} => {PurgeUnusedAccountsPeriodInDays}"); + sb.AppendLine($"{nameof(MaxCharaDataByUser)} => {MaxCharaDataByUser}"); + return sb.ToString(); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/ServicesConfiguration.cs b/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/ServicesConfiguration.cs new file mode 100644 index 0000000..70e0b23 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/ServicesConfiguration.cs @@ -0,0 +1,21 @@ +using System.Text; + +namespace MareSynchronosShared.Utils.Configuration; + +public class ServicesConfiguration : MareConfigurationBase +{ + public string DiscordBotToken { get; set; } = string.Empty; + public ulong? DiscordChannelForMessages { get; set; } = null; + public ulong? DiscordChannelForReports { get; set; } = null; + + public override string ToString() + { + StringBuilder sb = new(); + sb.AppendLine(base.ToString()); + sb.AppendLine($"{nameof(DiscordBotToken)} => {DiscordBotToken}"); + sb.AppendLine($"{nameof(MainServerAddress)} => {MainServerAddress}"); + sb.AppendLine($"{nameof(DiscordChannelForMessages)} => {DiscordChannelForMessages}"); + sb.AppendLine($"{nameof(DiscordChannelForReports)} => {DiscordChannelForReports}"); + return sb.ToString(); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/StaticFilesServerConfiguration.cs b/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/StaticFilesServerConfiguration.cs new file mode 100644 index 0000000..cd13538 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Utils/Configuration/StaticFilesServerConfiguration.cs @@ -0,0 +1,67 @@ +using MareSynchronosShared.Utils; +using System.Text; + +namespace MareSynchronosShared.Utils.Configuration; + +public class StaticFilesServerConfiguration : MareConfigurationBase +{ + public bool IsDistributionNode { get; set; } = false; + public bool NotifyMainServerDirectly { get; set; } = false; + public Uri MainFileServerAddress { get; set; } = null; + public Uri DistributionFileServerAddress { get; set; } = null; + public bool DistributionFileServerForceHTTP2 { get; set; } = false; + public int ForcedDeletionOfFilesAfterHours { get; set; } = -1; + public double CacheSizeHardLimitInGiB { get; set; } = -1; + public int MinimumFileRetentionPeriodInDays { get; set; } = 7; + public int UnusedFileRetentionPeriodInDays { get; set; } = 14; + public string CacheDirectory { get; set; } + public int DownloadQueueSize { get; set; } = 50; + public int DownloadTimeoutSeconds { get; set; } = 5; + public int DownloadQueueReleaseSeconds { get; set; } = 15; + public int DownloadQueueClearLimit { get; set; } = 15000; + public int CleanupCheckInMinutes { get; set; } = 15; + public bool UseColdStorage { get; set; } = false; + public string ColdStorageDirectory { get; set; } = null; + public double ColdStorageSizeHardLimitInGiB { get; set; } = -1; + public int ColdStorageMinimumFileRetentionPeriodInDays { get; set; } = 30; + public int ColdStorageUnusedFileRetentionPeriodInDays { get; set; } = 30; + public double CacheSmallSizeThresholdKiB { get; set; } = 64; + public double CacheLargeSizeThresholdKiB { get; set; } = 1024; + [RemoteConfiguration] + public Uri CdnFullUrl { get; set; } = null; + [RemoteConfiguration] + public List CdnShardConfiguration { get; set; } = new(); + + public bool UseXAccelRedirect { get; set; } = false; + public string XAccelRedirectPrefix { get; set; } = "/_internal/mare-files/"; + public bool UseSSI { get; set; } = false; + public string SSIContentType { get; set; } = "application/x-block-file-list"; + + public override string ToString() + { + StringBuilder sb = new(); + sb.AppendLine(base.ToString()); + sb.AppendLine($"{nameof(IsDistributionNode)} => {IsDistributionNode}"); + sb.AppendLine($"{nameof(NotifyMainServerDirectly)} => {NotifyMainServerDirectly}"); + sb.AppendLine($"{nameof(MainFileServerAddress)} => {MainFileServerAddress}"); + sb.AppendLine($"{nameof(DistributionFileServerAddress)} => {DistributionFileServerAddress}"); + sb.AppendLine($"{nameof(DistributionFileServerForceHTTP2)} => {DistributionFileServerForceHTTP2}"); + sb.AppendLine($"{nameof(ForcedDeletionOfFilesAfterHours)} => {ForcedDeletionOfFilesAfterHours}"); + sb.AppendLine($"{nameof(CacheSizeHardLimitInGiB)} => {CacheSizeHardLimitInGiB}"); + sb.AppendLine($"{nameof(UseColdStorage)} => {UseColdStorage}"); + sb.AppendLine($"{nameof(ColdStorageDirectory)} => {ColdStorageDirectory}"); + sb.AppendLine($"{nameof(ColdStorageSizeHardLimitInGiB)} => {ColdStorageSizeHardLimitInGiB}"); + sb.AppendLine($"{nameof(ColdStorageMinimumFileRetentionPeriodInDays)} => {ColdStorageMinimumFileRetentionPeriodInDays}"); + sb.AppendLine($"{nameof(ColdStorageUnusedFileRetentionPeriodInDays)} => {ColdStorageUnusedFileRetentionPeriodInDays}"); + sb.AppendLine($"{nameof(MinimumFileRetentionPeriodInDays)} => {MinimumFileRetentionPeriodInDays}"); + sb.AppendLine($"{nameof(UnusedFileRetentionPeriodInDays)} => {UnusedFileRetentionPeriodInDays}"); + sb.AppendLine($"{nameof(CacheSmallSizeThresholdKiB)} => {CacheSmallSizeThresholdKiB}"); + sb.AppendLine($"{nameof(CacheLargeSizeThresholdKiB)} => {CacheLargeSizeThresholdKiB}"); + sb.AppendLine($"{nameof(CacheDirectory)} => {CacheDirectory}"); + sb.AppendLine($"{nameof(DownloadQueueSize)} => {DownloadQueueSize}"); + sb.AppendLine($"{nameof(DownloadQueueReleaseSeconds)} => {DownloadQueueReleaseSeconds}"); + sb.AppendLine($"{nameof(CdnShardConfiguration)} => {string.Join(", ", CdnShardConfiguration)}"); + sb.AppendLine($"{nameof(UseXAccelRedirect)} => {UseXAccelRedirect}"); + return sb.ToString(); + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/IdBasedUserIdProvider.cs b/MareSynchronosServer/MareSynchronosShared/Utils/IdBasedUserIdProvider.cs new file mode 100644 index 0000000..45d592f --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Utils/IdBasedUserIdProvider.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.SignalR; + +namespace MareSynchronosShared.Utils; + +public class IdBasedUserIdProvider : IUserIdProvider +{ + public string GetUserId(HubConnectionContext context) + { + return context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, MareClaimTypes.Uid, StringComparison.Ordinal))?.Value; + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/MareClaimTypes.cs b/MareSynchronosServer/MareSynchronosShared/Utils/MareClaimTypes.cs new file mode 100644 index 0000000..7641e9e --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Utils/MareClaimTypes.cs @@ -0,0 +1,10 @@ +namespace MareSynchronosShared.Utils; + +public static class MareClaimTypes +{ + public const string Uid = "uid"; + public const string Alias = "alias"; + public const string CharaIdent = "character_identification"; + public const string Internal = "internal"; + public const string Continent = "continent"; +} diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/RemoteConfigurationAttribute.cs b/MareSynchronosServer/MareSynchronosShared/Utils/RemoteConfigurationAttribute.cs new file mode 100644 index 0000000..dd7ee5b --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Utils/RemoteConfigurationAttribute.cs @@ -0,0 +1,4 @@ +namespace MareSynchronosShared.Utils; + +[AttributeUsage(AttributeTargets.Property)] +public class RemoteConfigurationAttribute : Attribute { } \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/ServerTokenGenerator.cs b/MareSynchronosServer/MareSynchronosShared/Utils/ServerTokenGenerator.cs new file mode 100644 index 0000000..d7fffff --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Utils/ServerTokenGenerator.cs @@ -0,0 +1,61 @@ +using MareSynchronosShared.Utils.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace MareSynchronosShared.Utils; + +public class ServerTokenGenerator +{ + private readonly IOptionsMonitor _configuration; + private readonly ILogger _logger; + + private Dictionary _tokenDictionary { get; set; } = new(StringComparer.Ordinal); + public string Token + { + get + { + var currentJwt = _configuration.CurrentValue.Jwt; + if (_tokenDictionary.TryGetValue(currentJwt, out var token)) + { + return token; + } + + return GenerateToken(); + } + } + + public ServerTokenGenerator(IOptionsMonitor configuration, ILogger logger) + { + _configuration = configuration; + _logger = logger; + } + + private string GenerateToken() + { + var signingKey = _configuration.CurrentValue.Jwt; + var authSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(signingKey)); + + var token = new SecurityTokenDescriptor() + { + Subject = new ClaimsIdentity(new List() + { + new Claim(MareClaimTypes.Uid, _configuration.CurrentValue.ShardName), + new Claim(MareClaimTypes.Internal, "true"), + }), + SigningCredentials = new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256Signature), + }; + + var handler = new JwtSecurityTokenHandler(); + var rawData = handler.CreateJwtSecurityToken(token).RawData; + + _tokenDictionary[signingKey] = rawData; + + _logger.LogInformation("Generated Token: {data}", rawData); + + return rawData; + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/SharedDbFunctions.cs b/MareSynchronosServer/MareSynchronosShared/Utils/SharedDbFunctions.cs new file mode 100644 index 0000000..587ce06 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Utils/SharedDbFunctions.cs @@ -0,0 +1,124 @@ +using MareSynchronosShared.Data; +using MareSynchronosShared.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace MareSynchronosShared.Utils; + +public static class SharedDbFunctions +{ + public static async Task<(bool, string)> MigrateOrDeleteGroup(MareDbContext context, Group group, List groupPairs, int maxGroupsByUser) + { + bool groupHasMigrated = false; + string newOwner = string.Empty; + foreach (var potentialNewOwner in groupPairs.OrderByDescending(p => p.IsModerator).ThenByDescending(p => p.IsPinned).ToList()) + { + groupHasMigrated = await TryMigrateGroup(context, group, potentialNewOwner.GroupUserUID, maxGroupsByUser).ConfigureAwait(false); + + if (groupHasMigrated) + { + newOwner = potentialNewOwner.GroupUserUID; + potentialNewOwner.IsPinned = true; + potentialNewOwner.IsModerator = false; + + await context.SaveChangesAsync().ConfigureAwait(false); + break; + } + } + + if (!groupHasMigrated) + { + context.GroupPairs.RemoveRange(groupPairs); + context.Groups.Remove(group); + + await context.SaveChangesAsync().ConfigureAwait(false); + } + + return (groupHasMigrated, newOwner); + } + + public static async Task PurgeUser(ILogger _logger, User user, MareDbContext dbContext, int maxGroupsByUser) + { + _logger.LogInformation("Purging user: {uid}", user.UID); + + var secondaryUsers = await dbContext.Auth.Include(u => u.User) + .Where(u => u.PrimaryUserUID == user.UID).Select(c => c.User).ToListAsync().ConfigureAwait(false); + + foreach (var secondaryUser in secondaryUsers) + { + await PurgeUser(_logger, secondaryUser, dbContext, maxGroupsByUser).ConfigureAwait(false); + } + + var lodestone = dbContext.LodeStoneAuth.SingleOrDefault(a => a.User.UID == user.UID); + + var userProfileData = await dbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == user.UID).ConfigureAwait(false); + + if (lodestone != null) + { + dbContext.Remove(lodestone); + } + + if (userProfileData != null) + { + dbContext.Remove(userProfileData); + } + + var auth = dbContext.Auth.Single(a => a.UserUID == user.UID); + + var ownPairData = dbContext.ClientPairs.Where(u => u.User.UID == user.UID).ToList(); + dbContext.ClientPairs.RemoveRange(ownPairData); + var otherPairData = dbContext.ClientPairs.Include(u => u.User) + .Where(u => u.OtherUser.UID == user.UID).ToList(); + dbContext.ClientPairs.RemoveRange(otherPairData); + + var userJoinedGroups = await dbContext.GroupPairs.Include(g => g.Group).Where(u => u.GroupUserUID == user.UID).ToListAsync().ConfigureAwait(false); + + foreach (var userGroupPair in userJoinedGroups) + { + bool ownerHasLeft = string.Equals(userGroupPair.Group.OwnerUID, user.UID, StringComparison.Ordinal); + + if (ownerHasLeft) + { + var groupPairs = await dbContext.GroupPairs.Where(g => g.GroupGID == userGroupPair.GroupGID && g.GroupUserUID != user.UID).ToListAsync().ConfigureAwait(false); + + if (!groupPairs.Any()) + { + _logger.LogInformation("Group {gid} has no new owner, deleting", userGroupPair.GroupGID); + dbContext.Groups.Remove(userGroupPair.Group); + } + else + { + _ = await MigrateOrDeleteGroup(dbContext, userGroupPair.Group, groupPairs, maxGroupsByUser).ConfigureAwait(false); + } + } + + dbContext.GroupPairs.Remove(userGroupPair); + + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + + var bannedinGroups = await dbContext.GroupBans.Where(u => u.BannedUserUID == user.UID).ToListAsync().ConfigureAwait(false); + + dbContext.GroupBans.RemoveRange(bannedinGroups); + + _logger.LogInformation("User purged: {uid}", user.UID); + + dbContext.Auth.Remove(auth); + dbContext.Users.Remove(user); + + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + + private static async Task TryMigrateGroup(MareDbContext context, Group group, string potentialNewOwnerUid, int maxGroupsByUser) + { + var newOwnerOwnedGroups = await context.Groups.CountAsync(g => g.OwnerUID == potentialNewOwnerUid).ConfigureAwait(false); + if (newOwnerOwnedGroups >= maxGroupsByUser) + { + return false; + } + group.OwnerUID = potentialNewOwnerUid; + group.Alias = null; + await context.SaveChangesAsync().ConfigureAwait(false); + return true; + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/StringUtils.cs b/MareSynchronosServer/MareSynchronosShared/Utils/StringUtils.cs new file mode 100644 index 0000000..ede0ec3 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Utils/StringUtils.cs @@ -0,0 +1,49 @@ +using System.Security.Cryptography; +using System.Text; + +namespace MareSynchronosShared.Utils; + +public static class StringUtils +{ + public static string GenerateRandomString(int length, string? allowableChars = null) + { + if (string.IsNullOrEmpty(allowableChars)) + allowableChars = @"ABCDEFGHJKLMNPQRSTUVWXYZ0123456789"; + + // Generate random data + var rnd = RandomNumberGenerator.GetBytes(length); + + // Generate the output string + var allowable = allowableChars.ToCharArray(); + var l = allowable.Length; + var chars = new char[length]; + for (var i = 0; i < length; i++) + chars[i] = allowable[rnd[i] % l]; + + return new string(chars); + } + + public static string Sha256String(string input) + { + using var sha256 = SHA256.Create(); + return BitConverter.ToString(sha256.ComputeHash(Encoding.UTF8.GetBytes(input))).Replace("-", "", StringComparison.OrdinalIgnoreCase); + } +} + +public static class ListUtils +{ + private static Random rng = new(); + + public static void Shuffle(this IList list) + { + int n = list.Count; + while (n > 1) + { + n--; + int k = rng.Next(n + 1); + T value = list[k]; + list[k] = list[n]; + list[n] = value; + } + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/.config/dotnet-tools.json b/MareSynchronosServer/MareSynchronosStaticFilesServer/.config/dotnet-tools.json new file mode 100644 index 0000000..98091c9 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "6.0.8", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/CacheController.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/CacheController.cs new file mode 100644 index 0000000..34d5b6e --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/CacheController.cs @@ -0,0 +1,51 @@ +using MareSynchronos.API.Routes; +using MareSynchronosStaticFilesServer.Services; +using MareSynchronosStaticFilesServer.Utils; +using Microsoft.AspNetCore.Mvc; +using System.Globalization; +using System.Text; + +namespace MareSynchronosStaticFilesServer.Controllers; + +[Route(MareFiles.Cache)] +public class CacheController : ControllerBase +{ + private readonly RequestBlockFileListResultFactory _requestBlockFileListResultFactory; + private readonly CachedFileProvider _cachedFileProvider; + private readonly RequestQueueService _requestQueue; + private readonly FileStatisticsService _fileStatisticsService; + + public CacheController(ILogger logger, RequestBlockFileListResultFactory requestBlockFileListResultFactory, + CachedFileProvider cachedFileProvider, RequestQueueService requestQueue, FileStatisticsService fileStatisticsService) : base(logger) + { + _requestBlockFileListResultFactory = requestBlockFileListResultFactory; + _cachedFileProvider = cachedFileProvider; + _requestQueue = requestQueue; + _fileStatisticsService = fileStatisticsService; + } + + [HttpGet(MareFiles.Cache_Get)] + public async Task GetFiles(Guid requestId) + { + _logger.LogDebug($"GetFile:{MareUser}:{requestId}"); + + if (!_requestQueue.IsActiveProcessing(requestId, MareUser, out var request)) return BadRequest(); + + _requestQueue.ActivateRequest(requestId); + + long requestSize = 0; + var fileList = new List(request.FileIds.Count); + + foreach (var file in request.FileIds) + { + var fi = await _cachedFileProvider.GetAndDownloadFile(file); + if (fi == null) continue; + requestSize += fi.Length; + fileList.Add(fi); + } + + _fileStatisticsService.LogRequest(requestSize); + + return _requestBlockFileListResultFactory.Create(requestId, fileList); + } +} diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/ControllerBase.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/ControllerBase.cs new file mode 100644 index 0000000..018ab85 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/ControllerBase.cs @@ -0,0 +1,18 @@ +using MareSynchronosShared.Utils; +using Microsoft.AspNetCore.Mvc; + +namespace MareSynchronosStaticFilesServer.Controllers; + +public class ControllerBase : Controller +{ + protected ILogger _logger; + + public ControllerBase(ILogger logger) + { + _logger = logger; + } + + protected string MareUser => HttpContext.User.Claims.First(f => string.Equals(f.Type, MareClaimTypes.Uid, StringComparison.Ordinal)).Value; + protected string Continent => HttpContext.User.Claims.FirstOrDefault(f => string.Equals(f.Type, MareClaimTypes.Continent, StringComparison.Ordinal))?.Value ?? "*"; + protected bool IsPriority => false; +} diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/DistributionController.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/DistributionController.cs new file mode 100644 index 0000000..f18dd10 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/DistributionController.cs @@ -0,0 +1,60 @@ +using MareSynchronos.API.Routes; +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils.Configuration; +using MareSynchronosStaticFilesServer.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace MareSynchronosStaticFilesServer.Controllers; + +[Route(MareFiles.Distribution)] +public class DistributionController : ControllerBase +{ + private readonly CachedFileProvider _cachedFileProvider; + private readonly IConfigurationService _configuration; + + public DistributionController(ILogger logger, CachedFileProvider cachedFileProvider, + IConfigurationService configuration) : base(logger) + { + _cachedFileProvider = cachedFileProvider; + _configuration = configuration; + } + + [HttpGet(MareFiles.Distribution_Get)] + [Authorize(Policy = "Internal")] + public async Task GetFile(string file) + { + _logger.LogInformation($"GetFile:{MareUser}:{file}"); + + var fi = await _cachedFileProvider.GetAndDownloadFile(file); + if (fi == null) return NotFound(); + + if (_configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseXAccelRedirect), false)) + { + var prefix = _configuration.GetValue(nameof(StaticFilesServerConfiguration.XAccelRedirectPrefix)); + Response.Headers.Append("X-Accel-Redirect", Path.Combine(prefix, file)); + return Ok(); + } + else + { + return PhysicalFile(fi.FullName, "application/octet-stream"); + } + } + + [HttpPost("touch")] + [Authorize(Policy = "Internal")] + public IActionResult TouchFiles([FromBody] string[] files) + { + _logger.LogInformation($"TouchFiles:{MareUser}:{files.Length}"); + + if (files.Length == 0) + return Ok(); + + Task.Run(() => { + foreach (var file in files) + _cachedFileProvider.TouchColdHash(file); + }).ConfigureAwait(false); + + return Ok(); + } +} diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/MainController.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/MainController.cs new file mode 100644 index 0000000..7e43ba0 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/MainController.cs @@ -0,0 +1,25 @@ +using MareSynchronos.API.Routes; +using MareSynchronosStaticFilesServer.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace MareSynchronosStaticFilesServer.Controllers; + +[Route(MareFiles.Main)] +public class MainController : ControllerBase +{ + private readonly IClientReadyMessageService _messageService; + + public MainController(ILogger logger, IClientReadyMessageService mareHub) : base(logger) + { + _messageService = mareHub; + } + + [HttpGet(MareFiles.Main_SendReady)] + [Authorize(Policy = "Internal")] + public IActionResult SendReadyToClients(string uid, Guid requestId) + { + _messageService.SendDownloadReady(uid, requestId); + return Ok(); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/RequestController.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/RequestController.cs new file mode 100644 index 0000000..031ab67 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/RequestController.cs @@ -0,0 +1,72 @@ +using MareSynchronos.API.Routes; +using MareSynchronosStaticFilesServer.Services; +using Microsoft.AspNetCore.Mvc; + +namespace MareSynchronosStaticFilesServer.Controllers; + +[Route(MareFiles.Request)] +public class RequestController : ControllerBase +{ + private readonly CachedFileProvider _cachedFileProvider; + private readonly RequestQueueService _requestQueue; + private readonly FilePreFetchService _preFetchService; + + public RequestController(ILogger logger, CachedFileProvider cachedFileProvider, RequestQueueService requestQueue, FilePreFetchService preFetchService) : base(logger) + { + _cachedFileProvider = cachedFileProvider; + _requestQueue = requestQueue; + _preFetchService = preFetchService; + } + + [HttpGet] + [Route(MareFiles.Request_Cancel)] + public async Task CancelQueueRequest(Guid requestId) + { + try + { + _requestQueue.RemoveFromQueue(requestId, MareUser, IsPriority); + return Ok(); + } + catch (OperationCanceledException) { return BadRequest(); } + } + + [HttpPost] + [Route(MareFiles.Request_Enqueue)] + public async Task PreRequestFilesAsync([FromBody] IEnumerable files) + { + try + { + var hashList = files.ToList(); + var fileList = new List(); + + foreach (var file in hashList) + { + _logger.LogDebug("Prerequested file: " + file); + var fileInfo = await _cachedFileProvider.DownloadFileWhenRequired(file).ConfigureAwait(false); + if (fileInfo != null) + fileList.Add(fileInfo); + } + + _preFetchService.PrefetchFiles(fileList); + + Guid g = Guid.NewGuid(); + await _requestQueue.EnqueueUser(new(g, MareUser, hashList), IsPriority, HttpContext.RequestAborted); + + return Ok(g); + } + catch (OperationCanceledException) { return BadRequest(); } + } + + [HttpGet] + [Route(MareFiles.Request_Check)] + public async Task CheckQueueAsync(Guid requestId, [FromBody] IEnumerable files) + { + try + { + if (!_requestQueue.StillEnqueued(requestId, MareUser, IsPriority)) + await _requestQueue.EnqueueUser(new(requestId, MareUser, files.ToList()), IsPriority, HttpContext.RequestAborted); + return Ok(); + } + catch (OperationCanceledException) { return BadRequest(); } + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/ServerFilesController.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/ServerFilesController.cs new file mode 100644 index 0000000..7f95358 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/ServerFilesController.cs @@ -0,0 +1,280 @@ +using K4os.Compression.LZ4.Streams; +using MareSynchronos.API.Dto.Files; +using MareSynchronos.API.Routes; +using MareSynchronos.API.SignalR; +using MareSynchronosServer.Hubs; +using MareSynchronosShared.Data; +using MareSynchronosShared.Metrics; +using MareSynchronosShared.Models; +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils.Configuration; +using MareSynchronosStaticFilesServer.Services; +using MareSynchronosStaticFilesServer.Utils; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using System.Collections.Concurrent; +using System.Security.Cryptography; +using System.Security.Policy; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace MareSynchronosStaticFilesServer.Controllers; + +[Route(MareFiles.ServerFiles)] +public class ServerFilesController : ControllerBase +{ + private static readonly SemaphoreSlim _fileLockDictLock = new(1); + private static readonly ConcurrentDictionary _fileUploadLocks = new(StringComparer.Ordinal); + private readonly string _basePath; + private readonly string _coldBasePath; + private readonly CachedFileProvider _cachedFileProvider; + private readonly IConfigurationService _configuration; + private readonly IHubContext _hubContext; + private readonly MareDbContext _mareDbContext; + private readonly MareMetrics _metricsClient; + + public ServerFilesController(ILogger logger, CachedFileProvider cachedFileProvider, + IConfigurationService configuration, + IHubContext hubContext, + MareDbContext mareDbContext, MareMetrics metricsClient) : base(logger) + { + _configuration = configuration; + _basePath = configuration.GetValue(nameof(StaticFilesServerConfiguration.CacheDirectory)); + if (_configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseColdStorage), false)) + _basePath = configuration.GetValue(nameof(StaticFilesServerConfiguration.ColdStorageDirectory)); + _cachedFileProvider = cachedFileProvider; + _hubContext = hubContext; + _mareDbContext = mareDbContext; + _metricsClient = metricsClient; + } + + [HttpPost(MareFiles.ServerFiles_DeleteAll)] + public async Task FilesDeleteAll() + { + return Ok(); + } + + [HttpGet(MareFiles.ServerFiles_GetSizes)] + public async Task FilesGetSizes([FromBody] List hashes) + { + var forbiddenFiles = await _mareDbContext.ForbiddenUploadEntries. + Where(f => hashes.Contains(f.Hash)).ToListAsync().ConfigureAwait(false); + List response = new(); + + var cacheFile = await _mareDbContext.Files.AsNoTracking().Where(f => hashes.Contains(f.Hash)).AsNoTracking().Select(k => new { k.Hash, k.Size }).AsNoTracking().ToListAsync().ConfigureAwait(false); + + var allFileShards = new List(_configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.CdnShardConfiguration), new List())); + + foreach (var file in cacheFile) + { + var forbiddenFile = forbiddenFiles.SingleOrDefault(f => string.Equals(f.Hash, file.Hash, StringComparison.OrdinalIgnoreCase)); + Uri? baseUrl = null; + + if (forbiddenFile == null) + { + List selectedShards = new(); + var matchingShards = allFileShards.Where(f => new Regex(f.FileMatch).IsMatch(file.Hash)).ToList(); + + if (string.Equals(Continent, "*", StringComparison.Ordinal)) + { + selectedShards = matchingShards; + } + else + { + selectedShards = matchingShards.Where(c => c.Continents.Contains(Continent, StringComparer.OrdinalIgnoreCase)).ToList(); + if (!selectedShards.Any()) selectedShards = matchingShards; + } + + var shard = selectedShards + .OrderBy(s => !s.Continents.Any() ? 0 : 1) + .ThenBy(s => s.Continents.Contains("*", StringComparer.Ordinal) ? 0 : 1) + .ThenBy(g => Guid.NewGuid()).FirstOrDefault(); + + baseUrl = shard?.CdnFullUrl ?? _configuration.GetValue(nameof(StaticFilesServerConfiguration.CdnFullUrl)); + } + + response.Add(new DownloadFileDto + { + FileExists = file.Size > 0, + ForbiddenBy = forbiddenFile?.ForbiddenBy ?? string.Empty, + IsForbidden = forbiddenFile != null, + Hash = file.Hash, + Size = file.Size, + Url = baseUrl?.ToString() ?? string.Empty, + }); + } + + return Ok(JsonSerializer.Serialize(response)); + } + + [HttpPost(MareFiles.ServerFiles_FilesSend)] + public async Task FilesSend([FromBody] FilesSendDto filesSendDto) + { + var userSentHashes = new HashSet(filesSendDto.FileHashes.Distinct(StringComparer.Ordinal).Select(s => string.Concat(s.Where(c => char.IsLetterOrDigit(c)))), StringComparer.Ordinal); + var notCoveredFiles = new Dictionary(StringComparer.Ordinal); + var forbiddenFiles = await _mareDbContext.ForbiddenUploadEntries.AsNoTracking().Where(f => userSentHashes.Contains(f.Hash)).AsNoTracking().ToDictionaryAsync(f => f.Hash, f => f).ConfigureAwait(false); + var existingFiles = await _mareDbContext.Files.AsNoTracking().Where(f => userSentHashes.Contains(f.Hash)).AsNoTracking().ToDictionaryAsync(f => f.Hash, f => f).ConfigureAwait(false); + + List fileCachesToUpload = new(); + foreach (var hash in userSentHashes) + { + // Skip empty file hashes, duplicate file hashes, forbidden file hashes and existing file hashes + if (string.IsNullOrEmpty(hash)) { continue; } + if (notCoveredFiles.ContainsKey(hash)) { continue; } + if (forbiddenFiles.ContainsKey(hash)) + { + notCoveredFiles[hash] = new UploadFileDto() + { + ForbiddenBy = forbiddenFiles[hash].ForbiddenBy, + Hash = hash, + IsForbidden = true, + }; + + continue; + } + if (existingFiles.TryGetValue(hash, out var file) && file.Uploaded) { continue; } + + notCoveredFiles[hash] = new UploadFileDto() + { + Hash = hash, + }; + } + + if (notCoveredFiles.Any(p => !p.Value.IsForbidden)) + { + await _hubContext.Clients.Users(filesSendDto.UIDs).SendAsync(nameof(IMareHub.Client_UserReceiveUploadStatus), new MareSynchronos.API.Dto.User.UserDto(new(MareUser))) + .ConfigureAwait(false); + } + + return Ok(JsonSerializer.Serialize(notCoveredFiles.Values.ToList())); + } + + [HttpPost(MareFiles.ServerFiles_Upload + "/{hash}")] + [RequestSizeLimit(200 * 1024 * 1024)] + public async Task UploadFile(string hash, CancellationToken requestAborted) + { + _logger.LogInformation("{user} uploading file {file}", MareUser, hash); + hash = hash.ToUpperInvariant(); + var existingFile = await _mareDbContext.Files.SingleOrDefaultAsync(f => f.Hash == hash); + if (existingFile != null) return Ok(); + + SemaphoreSlim? fileLock = null; + bool successfullyWaited = false; + while (!successfullyWaited && !requestAborted.IsCancellationRequested) + { + lock (_fileUploadLocks) + { + if (!_fileUploadLocks.TryGetValue(hash, out fileLock)) + _fileUploadLocks[hash] = fileLock = new SemaphoreSlim(1); + } + + try + { + await fileLock.WaitAsync(requestAborted).ConfigureAwait(false); + successfullyWaited = true; + } + catch (ObjectDisposedException) + { + _logger.LogWarning("Semaphore disposed for {hash}, recreating", hash); + } + } + + try + { + var existingFileCheck2 = await _mareDbContext.Files.SingleOrDefaultAsync(f => f.Hash == hash); + if (existingFileCheck2 != null) + { + return Ok(); + } + + var path = FilePathUtil.GetFilePath(_basePath, hash); + var tmpPath = path + ".tmp"; + long compressedSize = -1; + + try + { + // Write incoming file to a temporary file while also hashing the decompressed content + + // Stream flow diagram: + // Request.Body ==> (Tee) ==> FileStream + // ==> CountedStream ==> LZ4DecoderStream ==> HashingStream ==> Stream.Null + + // Reading via TeeStream causes the request body to be copied to tmpPath + using var tmpFileStream = new FileStream(tmpPath, FileMode.Create); + using var teeStream = new TeeStream(Request.Body, tmpFileStream); + teeStream.DisposeUnderlying = false; + // Read via CountedStream to count the number of compressed bytes + using var countStream = new CountedStream(teeStream); + countStream.DisposeUnderlying = false; + + // The decompressed file content is read through LZ4DecoderStream, and written out to HashingStream + using var decStream = LZ4Stream.Decode(countStream, extraMemory: 0, leaveOpen: true); + // HashingStream simply hashes the decompressed bytes without writing them anywhere + using var hashStream = new HashingStream(Stream.Null, SHA1.Create()); + hashStream.DisposeUnderlying = false; + + await decStream.CopyToAsync(hashStream, requestAborted).ConfigureAwait(false); + decStream.Close(); + + var hashString = BitConverter.ToString(hashStream.Finish()) + .Replace("-", "", StringComparison.Ordinal).ToUpperInvariant(); + if (!string.Equals(hashString, hash, StringComparison.Ordinal)) + throw new InvalidOperationException($"Hash does not match file, computed: {hashString}, expected: {hash}"); + + compressedSize = countStream.BytesRead; + + // File content is verified -- move it to its final location + System.IO.File.Move(tmpPath, path, true); + } + catch + { + try + { + System.IO.File.Delete(tmpPath); + } + catch { } + throw; + } + + // update on db + await _mareDbContext.Files.AddAsync(new FileCache() + { + Hash = hash, + UploadDate = DateTime.UtcNow, + UploaderUID = MareUser, + Size = compressedSize, + Uploaded = true + }).ConfigureAwait(false); + await _mareDbContext.SaveChangesAsync().ConfigureAwait(false); + + _metricsClient.IncGauge(MetricsAPI.GaugeFilesTotal, 1); + _metricsClient.IncGauge(MetricsAPI.GaugeFilesTotalSize, compressedSize); + + _fileUploadLocks.TryRemove(hash, out _); + + return Ok(); + } + catch (Exception e) + { + _logger.LogError(e, "Error during file upload"); + return BadRequest(); + } + finally + { + try + { + fileLock?.Release(); + fileLock?.Dispose(); + } + catch (ObjectDisposedException) + { + // it's disposed whatever + } + finally + { + _fileUploadLocks.TryRemove(hash, out _); + } + } + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/DummyHub.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/DummyHub.cs new file mode 100644 index 0000000..afcb665 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/DummyHub.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.SignalR; + +// this is a very hacky way to attach this file server to the main mare hub signalr instance via redis +// signalr publishes the namespace and hubname into the redis backend so this needs to be equal to the original +// but I don't need to reimplement the hub completely as I only exclusively use it for internal connection calling +// from the queue service so I keep the namespace and name of the class the same so it can connect to the same channel +// if anyone finds a better way to do this let me know + +#pragma warning disable IDE0130 // Namespace does not match folder structure +#pragma warning disable MA0048 // File name must match type name +namespace MareSynchronosServer.Hubs; +public class MareHub : Hub +{ + public override Task OnConnectedAsync() + { + throw new NotSupportedException(); + } + + public override Task OnDisconnectedAsync(Exception exception) + { + throw new NotSupportedException(); + } +} +#pragma warning restore IDE0130 // Namespace does not match folder structure +#pragma warning restore MA0048 // File name must match type name \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/MareSynchronosStaticFilesServer.csproj b/MareSynchronosServer/MareSynchronosStaticFilesServer/MareSynchronosStaticFilesServer.csproj new file mode 100644 index 0000000..b1243f1 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/MareSynchronosStaticFilesServer.csproj @@ -0,0 +1,38 @@ + + + + net8.0 + enable + + + + + + + + + + + Never + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Program.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Program.cs new file mode 100644 index 0000000..42da70d --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Program.cs @@ -0,0 +1,43 @@ +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils.Configuration; + +namespace MareSynchronosStaticFilesServer; + +public class Program +{ + public static void Main(string[] args) + { + var hostBuilder = CreateHostBuilder(args); + var host = hostBuilder.Build(); + + using (var scope = host.Services.CreateScope()) + { + var options = host.Services.GetService>(); + var optionsServer = host.Services.GetService>(); + var logger = host.Services.GetService>(); + logger.LogInformation("Loaded MareSynchronos Static Files Server Configuration (IsMain: {isMain})", options.IsMain); + logger.LogInformation(options.ToString()); + logger.LogInformation("Loaded MareSynchronos Server Auth Configuration (IsMain: {isMain})", optionsServer.IsMain); + logger.LogInformation(optionsServer.ToString()); + } + + host.Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + var loggerFactory = LoggerFactory.Create(builder => + { + builder.ClearProviders(); + builder.AddConsole(); + }); + var logger = loggerFactory.CreateLogger(); + return Host.CreateDefaultBuilder(args) + .UseSystemd() + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseContentRoot(AppContext.BaseDirectory); + webBuilder.UseStartup(ctx => new Startup(ctx.Configuration, logger)); + }); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Properties/launchSettings.json b/MareSynchronosServer/MareSynchronosStaticFilesServer/Properties/launchSettings.json new file mode 100644 index 0000000..6ba3599 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:21378", + "sslPort": 44331 + } + }, + "profiles": { + "MareSynchronosStaticFilesServer": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7094;http://localhost:5094", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/CachedFileProvider.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/CachedFileProvider.cs new file mode 100644 index 0000000..bc0c987 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/CachedFileProvider.cs @@ -0,0 +1,243 @@ +using MareSynchronosShared.Metrics; +using MareSynchronosShared.Services; +using MareSynchronosStaticFilesServer.Utils; +using System.Collections.Concurrent; +using System.Net.Http.Headers; +using MareSynchronosShared.Utils; +using MareSynchronos.API.Routes; +using MareSynchronosShared.Utils.Configuration; + +namespace MareSynchronosStaticFilesServer.Services; + +public sealed class CachedFileProvider : IDisposable +{ + private readonly IConfigurationService _configuration; + private readonly ILogger _logger; + private readonly FileStatisticsService _fileStatisticsService; + private readonly MareMetrics _metrics; + private readonly ServerTokenGenerator _generator; + private readonly ITouchHashService _touchService; + private readonly Uri _remoteCacheSourceUri; + private readonly bool _useColdStorage; + private readonly string _hotStoragePath; + private readonly string _coldStoragePath; + private readonly ConcurrentDictionary _currentTransfers = new(StringComparer.Ordinal); + private readonly HttpClient _httpClient; + private readonly SemaphoreSlim _downloadSemaphore = new(1, 1); + private bool _disposed; + + private bool IsMainServer => _remoteCacheSourceUri == null && _isDistributionServer; + private bool _isDistributionServer; + + public CachedFileProvider(IConfigurationService configuration, ILogger logger, + FileStatisticsService fileStatisticsService, MareMetrics metrics, ServerTokenGenerator generator, ITouchHashService touchService) + { + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + _configuration = configuration; + _logger = logger; + _fileStatisticsService = fileStatisticsService; + _metrics = metrics; + _generator = generator; + _touchService = touchService; + _remoteCacheSourceUri = configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.DistributionFileServerAddress), null); + _isDistributionServer = configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.IsDistributionNode), false); + _useColdStorage = configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseColdStorage), false); + _hotStoragePath = configuration.GetValue(nameof(StaticFilesServerConfiguration.CacheDirectory)); + _coldStoragePath = configuration.GetValue(nameof(StaticFilesServerConfiguration.ColdStorageDirectory)); + _httpClient = new(); + _httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronosServer", "1.0.0.0")); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _httpClient?.Dispose(); + } + + private async Task DownloadTask(string hash) + { + var destinationFilePath = FilePathUtil.GetFilePath(_useColdStorage ? _coldStoragePath : _hotStoragePath, hash); + + // if cold storage is not configured or file not found or error is present try to download file from remote + var downloadUrl = MareFiles.DistributionGetFullPath(_remoteCacheSourceUri, hash); + _logger.LogInformation("Did not find {hash}, downloading from {server}", hash, downloadUrl); + + using var requestMessage = new HttpRequestMessage(HttpMethod.Get, downloadUrl); + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _generator.Token); + if (_configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.DistributionFileServerForceHTTP2), false)) + { + requestMessage.Version = new Version(2, 0); + requestMessage.VersionPolicy = HttpVersionPolicy.RequestVersionExact; + } + HttpResponseMessage? response = null; + + try + { + response = await _httpClient.SendAsync(requestMessage).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to download {url}", downloadUrl); + response?.Dispose(); + return; + } + + var tempFileName = destinationFilePath + ".dl"; + var fileStream = new FileStream(tempFileName, FileMode.Create, FileAccess.ReadWrite); + var bufferSize = 4096; + var buffer = new byte[bufferSize]; + + var bytesRead = 0; + using var content = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + while ((bytesRead = await content.ReadAsync(buffer).ConfigureAwait(false)) > 0) + { + await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead)).ConfigureAwait(false); + } + await fileStream.FlushAsync().ConfigureAwait(false); + await fileStream.DisposeAsync().ConfigureAwait(false); + File.Move(tempFileName, destinationFilePath, true); + + _metrics.IncGauge(_useColdStorage ? MetricsAPI.GaugeFilesTotalColdStorage : MetricsAPI.GaugeFilesTotal); + _metrics.IncGauge(_useColdStorage ? MetricsAPI.GaugeFilesTotalSizeColdStorage : MetricsAPI.GaugeFilesTotalSize, new FileInfo(destinationFilePath).Length); + response.Dispose(); + } + + private bool TryCopyFromColdStorage(string hash, string destinationFilePath) + { + if (!_useColdStorage) return false; + + if (string.IsNullOrEmpty(_coldStoragePath)) return false; + + var coldStorageFilePath = FilePathUtil.GetFilePath(_coldStoragePath, hash); + if (!File.Exists(coldStorageFilePath)) return false; + + try + { + _logger.LogDebug("Copying {hash} from cold storage: {path}", hash, coldStorageFilePath); + var tempFileName = destinationFilePath + ".dl"; + File.Copy(coldStorageFilePath, tempFileName, true); + File.Move(tempFileName, destinationFilePath, true); + var destinationFile = new FileInfo(destinationFilePath); + _metrics.IncGauge(MetricsAPI.GaugeFilesTotal); + _metrics.IncGauge(MetricsAPI.GaugeFilesTotalSize, new FileInfo(destinationFilePath).Length); + return true; + } + catch (Exception ex) + { + // Recover from a fairly common race condition -- max wait time is 75ms + // Having TryCopyFromColdStorage protected by the downloadtask mutex doesn't work for some reason? + for (int retry = 0; retry < 5; ++retry) + { + Thread.Sleep(5 + retry * 5); + if (File.Exists(destinationFilePath)) + return true; + } + _logger.LogWarning(ex, "Could not copy {coldStoragePath} from cold storage", coldStorageFilePath); + } + + return false; + } + + // Returns FileInfo ONLY if the hot file was immediately available without downloading + // Since the intended use is for pre-fetching files from hot storage, this is exactly what we need anyway + public async Task DownloadFileWhenRequired(string hash) + { + var fi = FilePathUtil.GetFileInfoForHash(_hotStoragePath, hash); + + if (fi != null && fi.Length != 0) + return fi; + + // first check cold storage + if (TryCopyFromColdStorage(hash, FilePathUtil.GetFilePath(_hotStoragePath, hash))) + return null; + + // no distribution server configured to download from + if (_remoteCacheSourceUri == null) + return null; + + await _downloadSemaphore.WaitAsync().ConfigureAwait(false); + if (!_currentTransfers.TryGetValue(hash, out var downloadTask) || (downloadTask?.IsCompleted ?? true)) + { + _currentTransfers[hash] = Task.Run(async () => + { + try + { + _metrics.IncGauge(MetricsAPI.GaugeFilesDownloadingFromCache); + await DownloadTask(hash).ConfigureAwait(false); + TryCopyFromColdStorage(hash, FilePathUtil.GetFilePath(_hotStoragePath, hash)); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error during Download Task for {hash}", hash); + } + finally + { + _metrics.DecGauge(MetricsAPI.GaugeFilesDownloadingFromCache); + _currentTransfers.Remove(hash, out _); + } + }); + } + _downloadSemaphore.Release(); + + return null; + } + + public async Task GetAndDownloadFile(string hash) + { + var fi = await DownloadFileWhenRequired(hash).ConfigureAwait(false); + + if (fi == null && _currentTransfers.TryGetValue(hash, out var downloadTask)) + { + try + { + using CancellationTokenSource cts = new(); + cts.CancelAfter(TimeSpan.FromSeconds(120)); + _metrics.IncGauge(MetricsAPI.GaugeFilesTasksWaitingForDownloadFromCache); + await downloadTask.WaitAsync(cts.Token).ConfigureAwait(false); + } + catch (Exception e) + { + _logger.LogWarning(e, "Failed while waiting for download task for {hash}", hash); + return null; + } + finally + { + _metrics.DecGauge(MetricsAPI.GaugeFilesTasksWaitingForDownloadFromCache); + } + } + + fi ??= FilePathUtil.GetFileInfoForHash(_hotStoragePath, hash); + + if (fi == null) + return null; + + fi.LastAccessTimeUtc = DateTime.UtcNow; + _touchService.TouchColdHash(hash); + + _fileStatisticsService.LogFile(hash, fi.Length); + + return fi; + } + + public async Task GetAndDownloadFileStream(string hash) + { + var fi = await GetAndDownloadFile(hash).ConfigureAwait(false); + return new FileStream(fi.FullName, FileMode.Open, FileAccess.Read, FileShare.Inheritable | FileShare.Read); + } + + public void TouchColdHash(string hash) + { + _touchService.TouchColdHash(hash); + } + + public bool AnyFilesDownloading(List hashes) + { + return hashes.Exists(_currentTransfers.Keys.Contains); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/ColdTouchHashService.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/ColdTouchHashService.cs new file mode 100644 index 0000000..f63d5ce --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/ColdTouchHashService.cs @@ -0,0 +1,82 @@ +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils.Configuration; +using MareSynchronosStaticFilesServer.Utils; +using System.Collections.Concurrent; + +namespace MareSynchronosStaticFilesServer.Services; + +// Perform access time updates for cold cache files accessed via hot cache or shard servers +public class ColdTouchHashService : ITouchHashService +{ + private readonly ILogger _logger; + private readonly IConfigurationService _configuration; + + private readonly bool _useColdStorage; + private readonly string _coldStoragePath; + + // Debounce multiple updates towards the same file + private readonly ConcurrentDictionary _lastUpdateTimesUtc = new(StringComparer.Ordinal); + private int _cleanupCounter = 0; + private object _cleanupLockObj = new(); + private const double _debounceTimeSecs = 900.0; + + public ColdTouchHashService(ILogger logger, IConfigurationService configuration) + { + _logger = logger; + _configuration = configuration; + _useColdStorage = configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseColdStorage), false); + _coldStoragePath = configuration.GetValue(nameof(StaticFilesServerConfiguration.ColdStorageDirectory)); + } + + public Task StartAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public void TouchColdHash(string hash) + { + if (!_useColdStorage) + return; + + var nowUtc = DateTime.UtcNow; + + // Clean up debounce dictionary regularly + if (_cleanupCounter++ >= 1000) + { + _cleanupCounter = 0; + if (Monitor.TryEnter(_cleanupLockObj)) + { + try + { + foreach (var entry in _lastUpdateTimesUtc.Where(entry => (nowUtc - entry.Value).TotalSeconds >= _debounceTimeSecs).ToList()) + _lastUpdateTimesUtc.TryRemove(entry.Key, out _); + } + finally + { + Monitor.Exit(_cleanupLockObj); + } + } + } + + // Ignore multiple updates within a time window of the first + if (_lastUpdateTimesUtc.TryGetValue(hash, out var lastUpdateTimeUtc) && (nowUtc - lastUpdateTimeUtc).TotalSeconds < _debounceTimeSecs) + return; + + var fileInfo = FilePathUtil.GetFileInfoForHash(_coldStoragePath, hash); + if (fileInfo != null) + { + _logger.LogTrace("Touching {fileName}", fileInfo.Name); + try + { + fileInfo.LastAccessTimeUtc = nowUtc; + } + catch (IOException) { return; } + _lastUpdateTimesUtc.TryAdd(hash, nowUtc); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/FileCleanupService.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/FileCleanupService.cs new file mode 100644 index 0000000..ff18cfe --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/FileCleanupService.cs @@ -0,0 +1,359 @@ +using ByteSizeLib; +using MareSynchronos.API.Dto.Files; +using MareSynchronosShared.Data; +using MareSynchronosShared.Metrics; +using MareSynchronosShared.Models; +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils.Configuration; +using MareSynchronosStaticFilesServer.Utils; +using MessagePack.Formatters; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Hosting.Systemd; + +namespace MareSynchronosStaticFilesServer.Services; + +public class FileCleanupService : IHostedService +{ + private readonly IConfigurationService _configuration; + private readonly ILogger _logger; + private readonly MareMetrics _metrics; + private readonly IServiceProvider _services; + + private readonly string _hotStoragePath; + private readonly string _coldStoragePath; + private readonly bool _isMain = false; + private readonly bool _isDistributionNode = false; + private readonly bool _useColdStorage = false; + private HashSet _orphanedFiles = new(StringComparer.Ordinal); + + private CancellationTokenSource _cleanupCts; + + private int HotStorageMinimumRetention => _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.MinimumFileRetentionPeriodInDays), 7); + private int HotStorageRetention => _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UnusedFileRetentionPeriodInDays), 14); + private double HotStorageSize => _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.CacheSizeHardLimitInGiB), -1.0); + + private int ColdStorageMinimumRetention => _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.ColdStorageMinimumFileRetentionPeriodInDays), 60); + private int ColdStorageRetention => _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.ColdStorageUnusedFileRetentionPeriodInDays), 60); + private double ColdStorageSize => _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.ColdStorageSizeHardLimitInGiB), -1.0); + + private double SmallSizeKiB => _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.CacheSmallSizeThresholdKiB), 64.0); + private double LargeSizeKiB => _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.CacheLargeSizeThresholdKiB), 1024.0); + + private int ForcedDeletionAfterHours => _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.ForcedDeletionOfFilesAfterHours), -1); + private int CleanupCheckMinutes => _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.CleanupCheckInMinutes), 15); + + private List GetAllHotFiles() => new DirectoryInfo(_hotStoragePath).GetFiles("*", SearchOption.AllDirectories) + .Where(f => f != null && f.Name.Length == 40) + .OrderBy(f => f.LastAccessTimeUtc).ToList(); + + private List GetAllColdFiles() => new DirectoryInfo(_coldStoragePath).GetFiles("*", SearchOption.AllDirectories) + .Where(f => f != null && f.Name.Length == 40) + .OrderBy(f => f.LastAccessTimeUtc).ToList(); + + private List GetTempFiles() => new DirectoryInfo(_useColdStorage ? _coldStoragePath : _hotStoragePath).GetFiles("*", SearchOption.AllDirectories) + .Where(f => f != null && (f.Name.EndsWith(".dl", StringComparison.InvariantCultureIgnoreCase) || f.Name.EndsWith(".tmp", StringComparison.InvariantCultureIgnoreCase))).ToList(); + + public FileCleanupService(MareMetrics metrics, ILogger logger, + IServiceProvider services, IConfigurationService configuration) + { + _metrics = metrics; + _logger = logger; + _services = services; + _configuration = configuration; + _useColdStorage = _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseColdStorage), false); + _hotStoragePath = configuration.GetValue(nameof(StaticFilesServerConfiguration.CacheDirectory)); + _coldStoragePath = configuration.GetValue(nameof(StaticFilesServerConfiguration.ColdStorageDirectory)); + _isDistributionNode = configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.IsDistributionNode), false); + _isMain = configuration.GetValue(nameof(StaticFilesServerConfiguration.MainFileServerAddress)) == null && _isDistributionNode; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Cleanup Service started"); + + InitializeGauges(); + + _cleanupCts = new(); + + _ = CleanUpTask(_cleanupCts.Token); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _cleanupCts.Cancel(); + + return Task.CompletedTask; + } + + private List CleanUpFilesBeyondSizeLimit(List files, double sizeLimit, double minTTL, double maxTTL, CancellationToken ct) + { + var removedFiles = new List(); + if (sizeLimit <= 0) + { + return removedFiles; + } + + var smallSize = SmallSizeKiB * 1024.0; + var largeSize = LargeSizeKiB * 1024.0; + var now = DateTime.Now; + + // Avoid nonsense in future calculations + if (smallSize < 0.0) + smallSize = 0.0; + + if (largeSize < smallSize) + largeSize = smallSize; + + if (minTTL < 0.0) + minTTL = 0.0; + + if (maxTTL < minTTL) + maxTTL = minTTL; + + // Calculates a deletion priority to prioritize deletion of larger files over a configured TTL range based on a file's size. + // This is intended to be applied to the hot cache, as the cost of recovering many small files is greater than a single large file. + // Example (minTTL=7, maxTTL=30): + // - A 10MB file was last accessed 5 days ago. Its calculated optimum TTL is 7 days. result = 0.7143 + // - A 50kB file was last accessed 10 days ago. Its calculated optimum TTL is 30 days. result = 0.3333 + // The larger file will be deleted with a higher priority than the smaller file. + double CalculateTTLProgression(FileInfo file) + { + var fileLength = (double)file.Length; + var fileAgeDays = (now - file.LastAccessTime).TotalDays; + var sizeNorm = Math.Clamp((fileLength - smallSize) / (largeSize - smallSize), 0.0, 1.0); + // Using Math.Sqrt(sizeNorm) would create a more logical scaling curve, but it barely matters + var ttlDayRange = (maxTTL - minTTL) * (1.0 - sizeNorm); + var daysPastMinTTL = Math.Max(fileAgeDays - minTTL, 0.0); + // There is some creativity in choosing an upper bound here: + // - With no upper bound, any file larger than `largeSize` is always the highest priority for deletion once it passes its calculated TTL + // - With 1.0 as an upper bound, all files older than `maxTTL` will have the same priority regardless of size + // - Using maxTTL/minTTL chooses a logical cut-off point where any files old enough to be affected would have been cleaned up already + var ttlProg = Math.Clamp(daysPastMinTTL / ttlDayRange, 0.0, maxTTL / minTTL); + return ttlProg; + } + + try + { + // Since we already have the file list sorted by access time, the list index is incorporated in to + // the dictionary key to preserve it as a secondary ordering + var sortedFiles = new PriorityQueue(); + + foreach (var (file, i) in files.Select((file, i) => ( file, i ))) + { + double ttlProg = CalculateTTLProgression(file); + sortedFiles.Enqueue(file, (-ttlProg, i)); + } + + _logger.LogInformation("Cleaning up files beyond the cache size limit of {cacheSizeLimit} GiB", sizeLimit); + var totalCacheSizeInBytes = files.Sum(s => s.Length); + long cacheSizeLimitInBytes = (long)ByteSize.FromGibiBytes(sizeLimit).Bytes; + while (totalCacheSizeInBytes > cacheSizeLimitInBytes && sortedFiles.Count != 0 && !ct.IsCancellationRequested) + { + var file = sortedFiles.Dequeue(); + totalCacheSizeInBytes -= file.Length; + _logger.LogInformation("Deleting {file} with size {size:N2}MiB", file.FullName, ByteSize.FromBytes(file.Length).MebiBytes); + file.Delete(); + removedFiles.Add(file.Name); + } + files.RemoveAll(f => removedFiles.Contains(f.Name, StringComparer.InvariantCultureIgnoreCase)); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error during cache size limit cleanup"); + } + + return removedFiles; + } + + private void CleanUpOrphanedFiles(HashSet allDbFileHashes, List allPhysicalFiles, CancellationToken ct) + { + // To avoid race conditions with file uploads, only delete files on a second pass + var newOrphanedFiles = new HashSet(StringComparer.Ordinal); + + foreach (var file in allPhysicalFiles.ToList()) + { + if (!allDbFileHashes.Contains(file.Name.ToUpperInvariant())) + { + _logger.LogInformation("File not in DB, marking: {fileName}", file.Name); + newOrphanedFiles.Add(file.FullName); + } + + ct.ThrowIfCancellationRequested(); + } + + foreach (var fullName in _orphanedFiles.Where(f => newOrphanedFiles.Contains(f))) + { + var name = Path.GetFileName(fullName); + File.Delete(fullName); + _logger.LogInformation("File still not in DB, deleting: {fileName}", name); + allPhysicalFiles.RemoveAll(f => f.FullName.Equals(fullName, StringComparison.InvariantCultureIgnoreCase)); + } + + _orphanedFiles = newOrphanedFiles; + } + + private List CleanUpOutdatedFiles(List files, int unusedRetention, int forcedDeletionAfterHours, CancellationToken ct) + { + var removedFiles = new List(); + try + { + _logger.LogInformation("Cleaning up files older than {filesOlderThanDays} days", unusedRetention); + if (forcedDeletionAfterHours > 0) + { + _logger.LogInformation("Cleaning up files written to longer than {hours}h ago", forcedDeletionAfterHours); + } + + var lastAccessCutoffTime = DateTime.Now.Subtract(TimeSpan.FromDays(unusedRetention)); + var forcedDeletionCutoffTime = DateTime.Now.Subtract(TimeSpan.FromHours(forcedDeletionAfterHours)); + + foreach (var file in files) + { + if (file.LastAccessTime < lastAccessCutoffTime) + { + _logger.LogInformation("File outdated: {fileName}, {fileSize:N2}MiB", file.Name, ByteSize.FromBytes(file.Length).MebiBytes); + file.Delete(); + removedFiles.Add(file.Name); + } + else if (forcedDeletionAfterHours > 0 && file.LastWriteTime < forcedDeletionCutoffTime) + { + _logger.LogInformation("File forcefully deleted: {fileName}, {fileSize:N2}MiB", file.Name, ByteSize.FromBytes(file.Length).MebiBytes); + file.Delete(); + removedFiles.Add(file.Name); + } + + ct.ThrowIfCancellationRequested(); + } + files.RemoveAll(f => removedFiles.Contains(f.Name, StringComparer.InvariantCultureIgnoreCase)); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error during file cleanup of old files"); + } + + return removedFiles; + } + + private void CleanUpTempFiles() + { + var pastTime = DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(20)); + var tempFiles = GetTempFiles(); + foreach (var tempFile in tempFiles.Where(f => f.LastWriteTimeUtc < pastTime)) + tempFile.Delete(); + } + + private async Task CleanUpTask(CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + try + { + using var scope = _services.CreateScope(); + using var dbContext = _isMain ? scope.ServiceProvider.GetService()! : null; + + HashSet allDbFileHashes = null; + + // Database operations only performed on main server + if (_isMain) + { + var allDbFiles = await dbContext.Files.ToListAsync(ct).ConfigureAwait(false); + allDbFileHashes = new HashSet(allDbFiles.Select(a => a.Hash.ToUpperInvariant()), StringComparer.Ordinal); + } + + if (_useColdStorage) + { + var coldFiles = GetAllColdFiles(); + var removedColdFiles = new List(); + + removedColdFiles.AddRange( + CleanUpOutdatedFiles(coldFiles, ColdStorageRetention, ForcedDeletionAfterHours, ct) + ); + removedColdFiles.AddRange( + CleanUpFilesBeyondSizeLimit(coldFiles, ColdStorageSize, ColdStorageMinimumRetention, ColdStorageRetention, ct) + ); + + // Remove cold storage files are deleted from the database, if we are the main file server + if (_isMain) + { + dbContext.Files.RemoveRange( + dbContext.Files.Where(f => removedColdFiles.Contains(f.Hash)) + ); + allDbFileHashes.ExceptWith(removedColdFiles); + CleanUpOrphanedFiles(allDbFileHashes, coldFiles, ct); + } + + // Remove hot copies of files now that the authoritative copy is gone + foreach (var removedFile in removedColdFiles) + { + var hotFile = FilePathUtil.GetFileInfoForHash(_hotStoragePath, removedFile); + hotFile?.Delete(); + } + + _metrics.SetGaugeTo(MetricsAPI.GaugeFilesTotalSizeColdStorage, coldFiles.Sum(f => { try { return f.Length; } catch { return 0; } })); + _metrics.SetGaugeTo(MetricsAPI.GaugeFilesTotalColdStorage, coldFiles.Count); + } + + var hotFiles = GetAllHotFiles(); + var removedHotFiles = new List(); + + removedHotFiles.AddRange( + CleanUpOutdatedFiles(hotFiles, HotStorageRetention, forcedDeletionAfterHours: _useColdStorage ? ForcedDeletionAfterHours : -1, ct) + ); + removedHotFiles.AddRange( + CleanUpFilesBeyondSizeLimit(hotFiles, HotStorageSize, HotStorageMinimumRetention, HotStorageRetention, ct) + ); + + if (_isMain) + { + // If cold storage is not active, then "hot" files are deleted from the database instead + if (!_useColdStorage) + { + dbContext.Files.RemoveRange( + dbContext.Files.Where(f => removedHotFiles.Contains(f.Hash)) + ); + allDbFileHashes.ExceptWith(removedHotFiles); + } + + CleanUpOrphanedFiles(allDbFileHashes, hotFiles, ct); + + await dbContext.SaveChangesAsync(ct).ConfigureAwait(false); + } + + _metrics.SetGaugeTo(MetricsAPI.GaugeFilesTotalSize, hotFiles.Sum(f => { try { return f.Length; } catch { return 0; } })); + _metrics.SetGaugeTo(MetricsAPI.GaugeFilesTotal, hotFiles.Count); + + CleanUpTempFiles(); + } + catch (Exception e) + { + _logger.LogError(e, "Error during cleanup task"); + } + + var cleanupCheckMinutes = CleanupCheckMinutes; + var now = DateTime.Now; + TimeOnly currentTime = new(now.Hour, now.Minute, now.Second); + TimeOnly futureTime = new(now.Hour, now.Minute - now.Minute % cleanupCheckMinutes, 0); + var span = futureTime.AddMinutes(cleanupCheckMinutes) - currentTime; + + _logger.LogInformation("File Cleanup Complete, next run at {date}", now.Add(span)); + await Task.Delay(span, ct).ConfigureAwait(false); + } + } + + private void InitializeGauges() + { + if (_useColdStorage) + { + var allFilesInColdStorageDir = GetAllColdFiles(); + + _metrics.SetGaugeTo(MetricsAPI.GaugeFilesTotalSizeColdStorage, allFilesInColdStorageDir.Sum(f => f.Length)); + _metrics.SetGaugeTo(MetricsAPI.GaugeFilesTotalColdStorage, allFilesInColdStorageDir.Count); + } + + var allFilesInHotStorage = GetAllHotFiles(); + + _metrics.SetGaugeTo(MetricsAPI.GaugeFilesTotalSize, allFilesInHotStorage.Sum(f => { try { return f.Length; } catch { return 0; } })); + _metrics.SetGaugeTo(MetricsAPI.GaugeFilesTotal, allFilesInHotStorage.Count); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/FilePreFetchService.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/FilePreFetchService.cs new file mode 100644 index 0000000..1dc8a6e --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/FilePreFetchService.cs @@ -0,0 +1,125 @@ +using Microsoft.Win32.SafeHandles; +using System.Runtime.InteropServices; +using System.Threading.Channels; + +namespace MareSynchronosStaticFilesServer.Services; + +// Pre-fetch files from cache storage in to memory +public class FilePreFetchService : IHostedService +{ + private struct PreFetchRequest + { + public FileInfo FileInfo; + public DateTime ExpiryUtc; + } + + private readonly ILogger _logger; + + private CancellationTokenSource _prefetchCts; + private readonly Channel _prefetchChannel; + + private const int _readAheadBytes = 8 * 1024 * 1024; // Maximum number of of bytes to prefetch per file (8MB) + private const int _preFetchTasks = 4; // Maximum number of tasks to process prefetches concurrently + + // Use readahead() on linux if its available + [DllImport("libc", EntryPoint = "readahead")] + static extern int LinuxReadAheadExternal(SafeFileHandle fd, Int64 offset, int count); + + private bool _hasLinuxReadAhead = true; + + public FilePreFetchService(ILogger logger) + { + _logger = logger; + _prefetchChannel = Channel.CreateUnbounded(); + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("File PreFetch Service started"); + _prefetchCts = new(); + for (int i = 0; i < _preFetchTasks; ++i) + _ = PrefetchTask(_prefetchCts.Token); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _prefetchCts.Cancel(); + return Task.CompletedTask; + } + + // Queue a list of hashes to be prefetched in a background task + public void PrefetchFiles(ICollection fileList) + { + if (!_hasLinuxReadAhead) + { + if (!_prefetchCts.IsCancellationRequested) + { + _logger.LogError("readahead() is not available - aborting File PreFetch Service"); + _prefetchCts.Cancel(); + } + return; + } + + var nowUtc = DateTime.UtcNow; + + // Expire prefetch requests that aren't picked up within 500ms + // By this point the request is probably already being served, or things are moving too slow to matter anyway + var expiry = nowUtc + TimeSpan.FromMilliseconds(500); + + foreach (var fileInfo in fileList) + { + _ = _prefetchChannel.Writer.TryWrite(new PreFetchRequest(){ + FileInfo = fileInfo, + ExpiryUtc = expiry, + }); + } + } + + private async Task PrefetchTask(CancellationToken ct) + { + var reader = _prefetchChannel.Reader; + + while (!ct.IsCancellationRequested) + { + try + { + var req = await reader.ReadAsync(ct).ConfigureAwait(false); + var nowUtc = DateTime.UtcNow; + + if (nowUtc >= req.ExpiryUtc) + { + _logger.LogDebug("Skipped expired prefetch for {hash}", req.FileInfo.Name); + continue; + } + + try + { + var fs = new FileStream(req.FileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.Inheritable | FileShare.Read); + + await using (fs.ConfigureAwait(false)) + { + try + { + _ = LinuxReadAheadExternal(fs.SafeFileHandle, 0, _readAheadBytes); + _logger.LogTrace("Prefetched {hash}", req.FileInfo.Name); + } + catch (EntryPointNotFoundException) + { + _hasLinuxReadAhead = false; + } + } + } + catch (IOException) { } + } + catch (OperationCanceledException) + { + continue; + } + catch (Exception e) + { + _logger.LogError(e, "Error during prefetch task"); + } + } + } +} diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/FileStatisticsService.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/FileStatisticsService.cs new file mode 100644 index 0000000..84487e8 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/FileStatisticsService.cs @@ -0,0 +1,94 @@ +using MareSynchronosShared.Metrics; +using System.Collections.Concurrent; + +namespace MareSynchronosStaticFilesServer.Services; + +public class FileStatisticsService : IHostedService +{ + private readonly MareMetrics _metrics; + private readonly ILogger _logger; + private CancellationTokenSource _resetCancellationTokenSource; + private ConcurrentDictionary _pastHourFiles = new(StringComparer.Ordinal); + private ConcurrentDictionary _pastDayFiles = new(StringComparer.Ordinal); + + public FileStatisticsService(MareMetrics metrics, ILogger logger) + { + _metrics = metrics; + _logger = logger; + } + + public void LogFile(string fileHash, long length) + { + if (!_pastHourFiles.ContainsKey(fileHash)) + { + _pastHourFiles[fileHash] = length; + _metrics.IncGauge(MetricsAPI.GaugeFilesUniquePastHour); + _metrics.IncGauge(MetricsAPI.GaugeFilesUniquePastHourSize, length); + } + if (!_pastDayFiles.ContainsKey(fileHash)) + { + _pastDayFiles[fileHash] = length; + _metrics.IncGauge(MetricsAPI.GaugeFilesUniquePastDay); + _metrics.IncGauge(MetricsAPI.GaugeFilesUniquePastDaySize, length); + } + } + + public void LogRequest(long requestSize) + { + _metrics.IncCounter(MetricsAPI.CounterFileRequests, 1); + _metrics.IncCounter(MetricsAPI.CounterFileRequestSize, requestSize); + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Starting FileStatisticsService"); + _resetCancellationTokenSource = new(); + _ = ResetHourlyFileData(); + _ = ResetDailyFileData(); + return Task.CompletedTask; + } + + public async Task ResetHourlyFileData() + { + while (!_resetCancellationTokenSource.Token.IsCancellationRequested) + { + _logger.LogInformation("Resetting 1h Data"); + + _pastHourFiles = new(StringComparer.Ordinal); + _metrics.SetGaugeTo(MetricsAPI.GaugeFilesUniquePastHour, 0); + _metrics.SetGaugeTo(MetricsAPI.GaugeFilesUniquePastHourSize, 0); + + var now = DateTime.UtcNow; + TimeOnly currentTime = new(now.Hour, now.Minute, now.Second); + TimeOnly futureTime = new(now.Hour, 0, 0); + var span = futureTime.AddHours(1) - currentTime; + + await Task.Delay(span, _resetCancellationTokenSource.Token).ConfigureAwait(false); + } + } + + public async Task ResetDailyFileData() + { + while (!_resetCancellationTokenSource.Token.IsCancellationRequested) + { + _logger.LogInformation("Resetting 24h Data"); + + _pastDayFiles = new(StringComparer.Ordinal); + _metrics.SetGaugeTo(MetricsAPI.GaugeFilesUniquePastDay, 0); + _metrics.SetGaugeTo(MetricsAPI.GaugeFilesUniquePastDaySize, 0); + + var now = DateTime.UtcNow; + DateTime midnight = new(new DateOnly(now.Date.Year, now.Date.Month, now.Date.Day), new(0, 0, 0)); + var span = midnight.AddDays(1) - now; + + await Task.Delay(span, _resetCancellationTokenSource.Token).ConfigureAwait(false); + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _resetCancellationTokenSource.Cancel(); + _logger.LogInformation("Stopping FileStatisticsService"); + return Task.CompletedTask; + } +} diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/IClientReadyMessageService.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/IClientReadyMessageService.cs new file mode 100644 index 0000000..932cf51 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/IClientReadyMessageService.cs @@ -0,0 +1,6 @@ +namespace MareSynchronosStaticFilesServer.Services; + +public interface IClientReadyMessageService +{ + void SendDownloadReady(string uid, Guid requestId); +} diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/ITouchHashService.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/ITouchHashService.cs new file mode 100644 index 0000000..679c19e --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/ITouchHashService.cs @@ -0,0 +1,6 @@ +namespace MareSynchronosStaticFilesServer.Services; + +public interface ITouchHashService : IHostedService +{ + void TouchColdHash(string hash); +} diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/MainClientReadyMessageService.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/MainClientReadyMessageService.cs new file mode 100644 index 0000000..17149c9 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/MainClientReadyMessageService.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.SignalR; +using MareSynchronos.API.SignalR; +using MareSynchronosServer.Hubs; + +namespace MareSynchronosStaticFilesServer.Services; + +public class MainClientReadyMessageService : IClientReadyMessageService +{ + private readonly ILogger _logger; + private readonly IHubContext _mareHub; + + public MainClientReadyMessageService(ILogger logger, IHubContext mareHub) + { + _logger = logger; + _mareHub = mareHub; + } + + public void SendDownloadReady(string uid, Guid requestId) + { + _ = Task.Run(async () => + { + _logger.LogDebug("Sending Client Ready for {uid}:{requestId} to SignalR", uid, requestId); + await _mareHub.Clients.User(uid).SendAsync(nameof(IMareHub.Client_DownloadReady), requestId).ConfigureAwait(false); + }); + } +} diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/RequestQueueService.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/RequestQueueService.cs new file mode 100644 index 0000000..7170226 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/RequestQueueService.cs @@ -0,0 +1,222 @@ +using MareSynchronosShared.Metrics; +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils.Configuration; +using MareSynchronosStaticFilesServer.Utils; +using System.Collections.Concurrent; +using System.Timers; + +namespace MareSynchronosStaticFilesServer.Services; + +public class RequestQueueService : IHostedService +{ + private readonly IClientReadyMessageService _clientReadyMessageService; + private readonly CachedFileProvider _cachedFileProvider; + private readonly ILogger _logger; + private readonly MareMetrics _metrics; + private readonly ConcurrentQueue _queue = new(); + private readonly ConcurrentQueue _priorityQueue = new(); + private readonly int _queueExpirationSeconds; + private readonly SemaphoreSlim _queueProcessingSemaphore = new(1); + private readonly UserQueueEntry[] _userQueueRequests; + private int _queueLimitForReset; + private readonly int _queueReleaseSeconds; + private System.Timers.Timer _queueTimer; + + public RequestQueueService(MareMetrics metrics, IConfigurationService configurationService, + ILogger logger, IClientReadyMessageService hubContext, CachedFileProvider cachedFileProvider) + { + _userQueueRequests = new UserQueueEntry[configurationService.GetValueOrDefault(nameof(StaticFilesServerConfiguration.DownloadQueueSize), 50)]; + _queueExpirationSeconds = configurationService.GetValueOrDefault(nameof(StaticFilesServerConfiguration.DownloadTimeoutSeconds), 5); + _queueLimitForReset = configurationService.GetValueOrDefault(nameof(StaticFilesServerConfiguration.DownloadQueueClearLimit), 15000); + _queueReleaseSeconds = configurationService.GetValueOrDefault(nameof(StaticFilesServerConfiguration.DownloadQueueReleaseSeconds), 15); + _metrics = metrics; + _logger = logger; + _clientReadyMessageService = hubContext; + _cachedFileProvider = cachedFileProvider; + } + + public void ActivateRequest(Guid request) + { + _logger.LogDebug("Activating request {guid}", request); + var req = _userQueueRequests.First(f => f != null && f.UserRequest.RequestId == request); + req.MarkActive(); + } + + public async Task EnqueueUser(UserRequest request, bool isPriority, CancellationToken token) + { + while (_queueProcessingSemaphore.CurrentCount == 0) + { + await Task.Delay(50, token).ConfigureAwait(false); + } + + _logger.LogDebug("Enqueueing req {guid} from {user} for {file}", request.RequestId, request.User, string.Join(", ", request.FileIds)); + + GetQueue(isPriority).Enqueue(request); + } + + public void FinishRequest(Guid request) + { + var req = _userQueueRequests.FirstOrDefault(f => f != null && f.UserRequest.RequestId == request); + if (req != null) + { + var idx = Array.IndexOf(_userQueueRequests, req); + _logger.LogDebug("Finishing Request {guid}, clearing slot {idx}", request, idx); + _userQueueRequests[idx] = null; + } + else + { + _logger.LogDebug("Request {guid} already cleared", request); + } + } + + public bool IsActiveProcessing(Guid request, string user, out UserRequest userRequest) + { + var userQueueRequest = _userQueueRequests.FirstOrDefault(u => u != null && u.UserRequest.RequestId == request && string.Equals(u.UserRequest.User, user, StringComparison.Ordinal)); + userRequest = userQueueRequest?.UserRequest ?? null; + return userQueueRequest != null && userRequest != null && userQueueRequest.ExpirationDate > DateTime.UtcNow; + } + + public void RemoveFromQueue(Guid requestId, string user, bool isPriority) + { + var existingRequest = GetQueue(isPriority).FirstOrDefault(f => f.RequestId == requestId && string.Equals(f.User, user, StringComparison.Ordinal)); + if (existingRequest == null) + { + var activeSlot = _userQueueRequests.FirstOrDefault(r => r != null && string.Equals(r.UserRequest.User, user, StringComparison.Ordinal) && r.UserRequest.RequestId == requestId); + if (activeSlot != null) + { + var idx = Array.IndexOf(_userQueueRequests, activeSlot); + if (idx >= 0) + { + _userQueueRequests[idx] = null; + } + } + } + else + { + existingRequest.IsCancelled = true; + } + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _queueTimer = new System.Timers.Timer(500); + _queueTimer.Elapsed += ProcessQueue; + _queueTimer.AutoReset = true; + _queueTimer.Start(); + return Task.CompletedTask; + } + + private ConcurrentQueue GetQueue(bool isPriority) => isPriority ? _priorityQueue : _queue; + + public bool StillEnqueued(Guid request, string user, bool isPriority) + { + return GetQueue(isPriority).Any(c => c.RequestId == request && string.Equals(c.User, user, StringComparison.Ordinal)); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _queueTimer.Stop(); + return Task.CompletedTask; + } + + private void DequeueIntoSlot(UserRequest userRequest, int slot) + { + _logger.LogDebug("Dequeueing {req} into {i}: {user} with {file}", userRequest.RequestId, slot, userRequest.User, string.Join(", ", userRequest.FileIds)); + _userQueueRequests[slot] = new(userRequest, DateTime.UtcNow.AddSeconds(_queueExpirationSeconds)); + _clientReadyMessageService.SendDownloadReady(userRequest.User, userRequest.RequestId); + } + + private void ProcessQueue(object src, ElapsedEventArgs e) + { + if (_queueProcessingSemaphore.CurrentCount == 0) return; + + _queueProcessingSemaphore.Wait(); + + try + { + if (_queue.Count(c => !c.IsCancelled) > _queueLimitForReset) + { + _queue.Clear(); + return; + } + + for (int i = 0; i < _userQueueRequests.Length; i++) + { + try + { + if (_userQueueRequests[i] != null + && (((!_userQueueRequests[i].IsActive && _userQueueRequests[i].ExpirationDate < DateTime.UtcNow)) + || (_userQueueRequests[i].IsActive && _userQueueRequests[i].ActivationDate < DateTime.UtcNow.Subtract(TimeSpan.FromSeconds(_queueReleaseSeconds)))) + ) + { + _logger.LogDebug("Expiring request {guid} slot {slot}", _userQueueRequests[i].UserRequest.RequestId, i); + _userQueueRequests[i] = null; + } + + if (_userQueueRequests[i] != null) continue; + + while (true) + { + if (!_priorityQueue.All(u => _cachedFileProvider.AnyFilesDownloading(u.FileIds)) + && _priorityQueue.TryDequeue(out var prioRequest)) + { + if (prioRequest.IsCancelled) + { + continue; + } + + if (_cachedFileProvider.AnyFilesDownloading(prioRequest.FileIds)) + { + _priorityQueue.Enqueue(prioRequest); + continue; + } + + DequeueIntoSlot(prioRequest, i); + break; + } + + if (!_queue.All(u => _cachedFileProvider.AnyFilesDownloading(u.FileIds)) + && _queue.TryDequeue(out var request)) + { + if (request.IsCancelled) + { + continue; + } + + if (_cachedFileProvider.AnyFilesDownloading(request.FileIds)) + { + _queue.Enqueue(request); + continue; + } + + DequeueIntoSlot(request, i); + break; + } + + break; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error during inside queue processing"); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Queue processing"); + } + finally + { + _queueProcessingSemaphore.Release(); + } + + _metrics.SetGaugeTo(MetricsAPI.GaugeQueueFree, _userQueueRequests.Count(c => c == null)); + _metrics.SetGaugeTo(MetricsAPI.GaugeQueueActive, _userQueueRequests.Count(c => c != null && c.IsActive)); + _metrics.SetGaugeTo(MetricsAPI.GaugeQueueInactive, _userQueueRequests.Count(c => c != null && !c.IsActive)); + _metrics.SetGaugeTo(MetricsAPI.GaugeDownloadQueue, _queue.Count(q => !q.IsCancelled)); + _metrics.SetGaugeTo(MetricsAPI.GaugeDownloadQueueCancelled, _queue.Count(q => q.IsCancelled)); + _metrics.SetGaugeTo(MetricsAPI.GaugeDownloadPriorityQueue, _priorityQueue.Count(q => !q.IsCancelled)); + _metrics.SetGaugeTo(MetricsAPI.GaugeDownloadPriorityQueueCancelled, _priorityQueue.Count(q => q.IsCancelled)); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/ShardClientReadyMessageService.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/ShardClientReadyMessageService.cs new file mode 100644 index 0000000..b8584f2 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/ShardClientReadyMessageService.cs @@ -0,0 +1,51 @@ +using MareSynchronos.API.Routes; +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils; +using MareSynchronosShared.Utils.Configuration; +using System.Net.Http.Headers; + +namespace MareSynchronosStaticFilesServer.Services; + +public class ShardClientReadyMessageService : IClientReadyMessageService +{ + private readonly ILogger _logger; + private readonly ServerTokenGenerator _tokenGenerator; + private readonly IConfigurationService _configurationService; + private readonly HttpClient _httpClient; + + public ShardClientReadyMessageService(ILogger logger, ServerTokenGenerator tokenGenerator, IConfigurationService configurationService) + { + _logger = logger; + _tokenGenerator = tokenGenerator; + _configurationService = configurationService; + _httpClient = new(); + _httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronosServer", "1.0.0.0")); + } + + public void SendDownloadReady(string uid, Guid requestId) + { + _ = Task.Run(async () => + { + var mainUrlConfigKey = _configurationService.GetValue(nameof(StaticFilesServerConfiguration.NotifyMainServerDirectly)) + ? nameof(StaticFilesServerConfiguration.MainServerAddress) + : nameof(StaticFilesServerConfiguration.MainFileServerAddress); + var mainUrl = _configurationService.GetValue(mainUrlConfigKey); + var path = MareFiles.MainSendReadyFullPath(mainUrl, uid, requestId); + using HttpRequestMessage msg = new() + { + RequestUri = path + }; + msg.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _tokenGenerator.Token); + + _logger.LogDebug("Sending Client Ready for {uid}:{requestId} to {path}", uid, requestId, path); + try + { + using var result = await _httpClient.SendAsync(msg).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failure to send for {uid}:{requestId}", uid, requestId); + } + }); + } +} diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/ShardTouchMessageService.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/ShardTouchMessageService.cs new file mode 100644 index 0000000..5a02881 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Services/ShardTouchMessageService.cs @@ -0,0 +1,132 @@ +using MareSynchronos.API.Routes; +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils; +using MareSynchronosShared.Utils.Configuration; +using System.Net.Http.Headers; + +namespace MareSynchronosStaticFilesServer.Services; + +// Notify distribution server of file hashes downloaded via shards, so they are not prematurely purged from its cold cache +public class ShardTouchMessageService : ITouchHashService +{ + private readonly ILogger _logger; + private readonly ServerTokenGenerator _tokenGenerator; + private readonly IConfigurationService _configuration; + private readonly HttpClient _httpClient; + private readonly Uri _remoteCacheSourceUri; + private readonly HashSet _touchHashSet = new(); + private readonly ColdTouchHashService _nestedService = null; + + private CancellationTokenSource _touchmsgCts; + + public ShardTouchMessageService(ILogger logger, ILogger nestedLogger, + ServerTokenGenerator tokenGenerator, IConfigurationService configuration) + { + _logger = logger; + _tokenGenerator = tokenGenerator; + _configuration = configuration; + _remoteCacheSourceUri = _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.DistributionFileServerAddress), null); + _httpClient = new(); + _httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronosServer", "1.0.0.0")); + + if (configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseColdStorage), false)) + { + _nestedService = new ColdTouchHashService(nestedLogger, configuration); + } + } + + public Task StartAsync(CancellationToken cancellationToken) + { + if (_remoteCacheSourceUri == null) + return Task.CompletedTask; + + _logger.LogInformation("Touch Message Service started"); + + _touchmsgCts = new(); + + _ = TouchMessageTask(_touchmsgCts.Token); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + if (_remoteCacheSourceUri == null) + return Task.CompletedTask; + + _touchmsgCts.Cancel(); + + return Task.CompletedTask; + } + + private async Task SendTouches(IEnumerable hashes) + { + var mainUrl = _remoteCacheSourceUri; + var path = new Uri(mainUrl, MareFiles.Distribution + "/touch"); + using HttpRequestMessage msg = new() + { + RequestUri = path + }; + msg.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _tokenGenerator.Token); + msg.Method = HttpMethod.Post; + msg.Content = JsonContent.Create(hashes); + if (_configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.DistributionFileServerForceHTTP2), false)) + { + msg.Version = new Version(2, 0); + msg.VersionPolicy = HttpVersionPolicy.RequestVersionExact; + } + + _logger.LogDebug("Sending remote touch to {path}", path); + try + { + using var result = await _httpClient.SendAsync(msg).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failure to send touches for {hashChunk}", hashes); + } + } + + private async Task TouchMessageTask(CancellationToken ct) + { + List hashes; + + while (!ct.IsCancellationRequested) + { + try + { + lock (_touchHashSet) + { + hashes = _touchHashSet.ToList(); + _touchHashSet.Clear(); + } + if (hashes.Count > 0) + await SendTouches(hashes).ConfigureAwait(false); + await Task.Delay(TimeSpan.FromSeconds(60), ct).ConfigureAwait(false); + } + catch (Exception e) + { + _logger.LogError(e, "Error during touch message task"); + } + } + + lock (_touchHashSet) + { + hashes = _touchHashSet.ToList(); + _touchHashSet.Clear(); + } + if (hashes.Count > 0) + await SendTouches(hashes).ConfigureAwait(false); + } + + public void TouchColdHash(string hash) + { + if (_nestedService != null) + _nestedService.TouchColdHash(hash); + + lock (_touchHashSet) + { + _touchHashSet.Add(hash); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Startup.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Startup.cs new file mode 100644 index 0000000..9c01a36 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Startup.cs @@ -0,0 +1,271 @@ +using MareSynchronosShared.Data; +using MareSynchronosShared.Metrics; +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils; +using MareSynchronosStaticFilesServer.Controllers; +using MareSynchronosStaticFilesServer.Services; +using MareSynchronosStaticFilesServer.Utils; +using MessagePack; +using MessagePack.Resolvers; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Prometheus; +using StackExchange.Redis.Extensions.Core.Configuration; +using StackExchange.Redis.Extensions.System.Text.Json; +using StackExchange.Redis; +using System.Net; +using System.Text; +using MareSynchronosShared.Utils.Configuration; + +namespace MareSynchronosStaticFilesServer; + +public class Startup +{ + private bool _isMain; + private bool _isDistributionNode; + private bool _hasDistributionUpstream; + private readonly ILogger _logger; + + public Startup(IConfiguration configuration, ILogger logger) + { + Configuration = configuration; + _logger = logger; + var mareSettings = Configuration.GetRequiredSection("MareSynchronos"); + _isDistributionNode = mareSettings.GetValue(nameof(StaticFilesServerConfiguration.IsDistributionNode), false); + _hasDistributionUpstream = !string.IsNullOrEmpty(mareSettings.GetValue(nameof(StaticFilesServerConfiguration.DistributionFileServerAddress), string.Empty)); + _isMain = string.IsNullOrEmpty(mareSettings.GetValue(nameof(StaticFilesServerConfiguration.MainFileServerAddress), string.Empty)) && _isDistributionNode; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddHttpContextAccessor(); + + services.AddLogging(); + + services.Configure(Configuration.GetRequiredSection("MareSynchronos")); + services.Configure(Configuration.GetRequiredSection("MareSynchronos")); + services.Configure(Configuration.GetSection("Kestrel")); + services.AddSingleton(Configuration); + + var mareConfig = Configuration.GetRequiredSection("MareSynchronos"); + + // metrics configuration + services.AddSingleton(m => new MareMetrics(m.GetService>(), new List + { + MetricsAPI.CounterFileRequests, + MetricsAPI.CounterFileRequestSize + }, new List + { + MetricsAPI.GaugeFilesTotalColdStorage, + MetricsAPI.GaugeFilesTotalSizeColdStorage, + MetricsAPI.GaugeFilesTotalSize, + MetricsAPI.GaugeFilesTotal, + MetricsAPI.GaugeFilesUniquePastDay, + MetricsAPI.GaugeFilesUniquePastDaySize, + MetricsAPI.GaugeFilesUniquePastHour, + MetricsAPI.GaugeFilesUniquePastHourSize, + MetricsAPI.GaugeCurrentDownloads, + MetricsAPI.GaugeDownloadQueue, + MetricsAPI.GaugeDownloadQueueCancelled, + MetricsAPI.GaugeDownloadPriorityQueue, + MetricsAPI.GaugeDownloadPriorityQueueCancelled, + MetricsAPI.GaugeQueueFree, + MetricsAPI.GaugeQueueInactive, + MetricsAPI.GaugeQueueActive, + MetricsAPI.GaugeFilesDownloadingFromCache, + MetricsAPI.GaugeFilesTasksWaitingForDownloadFromCache + })); + + // generic services + services.AddSingleton(); + services.AddHostedService(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(p => p.GetService()); + services.AddSingleton(); + services.AddHostedService(p => p.GetService()); + services.AddHostedService(m => m.GetService()); + services.AddSingleton, MareConfigurationServiceClient>(); + services.AddHostedService(p => (MareConfigurationServiceClient)p.GetService>()); + + // specific services + if (_isMain) + { + services.AddSingleton(); + services.AddSingleton, MareConfigurationServiceServer>(); + services.AddDbContextPool(options => + { + options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder => + { + builder.MigrationsHistoryTable("_efmigrationshistory", "public"); + }).UseSnakeCaseNamingConvention(); + options.EnableThreadSafetyChecks(false); + }, mareConfig.GetValue(nameof(MareConfigurationBase.DbContextPoolSize), 1024)); + + var signalRServiceBuilder = services.AddSignalR(hubOptions => + { + hubOptions.MaximumReceiveMessageSize = long.MaxValue; + hubOptions.EnableDetailedErrors = true; + hubOptions.MaximumParallelInvocationsPerClient = 10; + hubOptions.StreamBufferCapacity = 200; + }).AddMessagePackProtocol(opt => + { + var resolver = CompositeResolver.Create(StandardResolverAllowPrivate.Instance, + BuiltinResolver.Instance, + AttributeFormatterResolver.Instance, + // replace enum resolver + DynamicEnumAsStringResolver.Instance, + DynamicGenericResolver.Instance, + DynamicUnionResolver.Instance, + DynamicObjectResolver.Instance, + PrimitiveObjectResolver.Instance, + // final fallback(last priority) + StandardResolver.Instance); + + opt.SerializerOptions = MessagePackSerializerOptions.Standard + .WithCompression(MessagePackCompression.Lz4Block) + .WithResolver(resolver); + }); + + // configure redis for SignalR + var redisConnection = mareConfig.GetValue(nameof(ServerConfiguration.RedisConnectionString), string.Empty); + signalRServiceBuilder.AddStackExchangeRedis(redisConnection, options => { }); + + var options = ConfigurationOptions.Parse(redisConnection); + + var endpoint = options.EndPoints[0]; + string address = ""; + int port = 0; + if (endpoint is DnsEndPoint dnsEndPoint) { address = dnsEndPoint.Host; port = dnsEndPoint.Port; } + if (endpoint is IPEndPoint ipEndPoint) { address = ipEndPoint.Address.ToString(); port = ipEndPoint.Port; } + var redisConfiguration = new RedisConfiguration() + { + AbortOnConnectFail = true, + KeyPrefix = "", + Hosts = new RedisHost[] + { + new RedisHost(){ Host = address, Port = port }, + }, + AllowAdmin = true, + ConnectTimeout = options.ConnectTimeout, + Database = 0, + Ssl = false, + Password = options.Password, + ServerEnumerationStrategy = new ServerEnumerationStrategy() + { + Mode = ServerEnumerationStrategy.ModeOptions.All, + TargetRole = ServerEnumerationStrategy.TargetRoleOptions.Any, + UnreachableServerAction = ServerEnumerationStrategy.UnreachableServerActionOptions.Throw, + }, + MaxValueLength = 1024, + PoolSize = mareConfig.GetValue(nameof(ServerConfiguration.RedisPool), 50), + SyncTimeout = options.SyncTimeout, + }; + + services.AddStackExchangeRedisExtensions(redisConfiguration); + } + else + { + services.AddSingleton(); + services.AddSingleton, MareConfigurationServiceClient>(); + services.AddHostedService(p => (MareConfigurationServiceClient)p.GetService>()); + } + + if (!_hasDistributionUpstream) + { + services.AddSingleton(); + services.AddHostedService(p => p.GetService()); + } + else + { + services.AddSingleton(); + services.AddHostedService(p => p.GetService()); + } + + // controller setup + services.AddControllers().ConfigureApplicationPartManager(a => + { + a.FeatureProviders.Remove(a.FeatureProviders.OfType().First()); + if (_isMain) + { + a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(MareStaticFilesServerConfigurationController), + typeof(CacheController), typeof(RequestController), typeof(ServerFilesController), + typeof(DistributionController), typeof(MainController))); + } + else if (_isDistributionNode) + { + a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(CacheController), typeof(RequestController), typeof(DistributionController))); + } + else + { + a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(CacheController), typeof(RequestController))); + } + }); + + // authentication and authorization + services.AddOptions(JwtBearerDefaults.AuthenticationScheme) + .Configure>((o, s) => + { + o.TokenValidationParameters = new() + { + ValidateIssuer = false, + ValidateLifetime = false, + ValidateAudience = false, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(s.GetValue(nameof(MareConfigurationBase.Jwt)))) + }; + }); + services.AddAuthentication(o => + { + o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(); + services.AddAuthorization(options => + { + options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + options.AddPolicy("Internal", new AuthorizationPolicyBuilder().RequireClaim(MareClaimTypes.Internal, "true").Build()); + }); + services.AddSingleton(); + + services.AddHealthChecks(); + services.AddHttpLogging(e => e = new Microsoft.AspNetCore.HttpLogging.HttpLoggingOptions()); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseHttpLogging(); + + app.UseRouting(); + + var config = app.ApplicationServices.GetRequiredService>(); + + var metricServer = new KestrelMetricServer(config.GetValueOrDefault(nameof(MareConfigurationBase.MetricsPort), 4981)); + metricServer.Start(); + + app.UseHttpMetrics(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(e => + { + if (_isMain) + { + e.MapHub("/dummyhub"); + } + + e.MapControllers(); + e.MapHealthChecks("/health").WithMetadata(new AllowAnonymousAttribute()); + }); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/CountedStream.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/CountedStream.cs new file mode 100644 index 0000000..d5412c5 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/CountedStream.cs @@ -0,0 +1,73 @@ + +namespace MareSynchronosStaticFilesServer.Utils; + +// Counts the number of bytes read/written to an underlying stream +public class CountedStream : Stream +{ + private readonly Stream _stream; + public long BytesRead { get; private set; } + public long BytesWritten { get; private set; } + public bool DisposeUnderlying = true; + + public Stream UnderlyingStream { get => _stream; } + + public CountedStream(Stream underlyingStream) + { + _stream = underlyingStream; + } + + protected override void Dispose(bool disposing) + { + if (!DisposeUnderlying) + return; + _stream.Dispose(); + } + + public override bool CanRead => _stream.CanRead; + public override bool CanSeek => _stream.CanSeek; + public override bool CanWrite => _stream.CanWrite; + public override long Length => _stream.Length; + + public override long Position { get => _stream.Position; set => _stream.Position = value; } + + public override void Flush() + { + _stream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + int n = _stream.Read(buffer, offset, count); + BytesRead += n; + return n; + } + + public async override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + int n = await _stream.ReadAsync(buffer, offset, count, cancellationToken); + BytesRead += n; + return n; + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _stream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + _stream.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + _stream.Write(buffer, offset, count); + BytesWritten += count; + } + + public async override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + await _stream.WriteAsync(buffer, offset, count, cancellationToken); + BytesWritten += count; + } +} diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/FilePathUtil.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/FilePathUtil.cs new file mode 100644 index 0000000..cd0d04d --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/FilePathUtil.cs @@ -0,0 +1,31 @@ +namespace MareSynchronosStaticFilesServer.Utils; + +public static partial class FilePathUtil +{ + public static FileInfo GetFileInfoForHash(string basePath, string hash) + { + if (hash.Length != 40 || !hash.All(char.IsAsciiLetterOrDigit)) throw new InvalidOperationException(); + + FileInfo fi = new(Path.Join(basePath, hash[0].ToString(), hash)); + if (!fi.Exists) + { + fi = new FileInfo(Path.Join(basePath, hash)); + if (!fi.Exists) + { + return null; + } + } + + return fi; + } + + public static string GetFilePath(string basePath, string hash) + { + if (hash.Length != 40 || !hash.All(char.IsAsciiLetterOrDigit)) throw new InvalidOperationException(); + + var dirPath = Path.Join(basePath, hash[0].ToString()); + var path = Path.Join(dirPath, hash); + if (!Directory.Exists(dirPath)) Directory.CreateDirectory(dirPath); + return path; + } +} diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/HashingStream.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/HashingStream.cs new file mode 100644 index 0000000..4fdcf5d --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/HashingStream.cs @@ -0,0 +1,82 @@ +using System.Security.Cryptography; + +namespace MareSynchronosStaticFilesServer.Utils; + +// Calculates the hash of content read or written to a stream +public class HashingStream : Stream +{ + private readonly Stream _stream; + private readonly HashAlgorithm _hashAlgo; + private bool _finished = false; + public bool DisposeUnderlying = true; + + public Stream UnderlyingStream { get => _stream; } + + public HashingStream(Stream underlyingStream, HashAlgorithm hashAlgo) + { + _stream = underlyingStream; + _hashAlgo = hashAlgo; + } + + protected override void Dispose(bool disposing) + { + if (!DisposeUnderlying) + return; + if (!_finished) + _stream.Dispose(); + _hashAlgo.Dispose(); + } + + public override bool CanRead => _stream.CanRead; + public override bool CanSeek => false; + public override bool CanWrite => _stream.CanWrite; + public override long Length => _stream.Length; + + public override long Position { get => _stream.Position; set => throw new NotSupportedException(); } + + public override void Flush() + { + _stream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (_finished) + throw new ObjectDisposedException("HashingStream"); + int n = _stream.Read(buffer, offset, count); + if (n > 0) + _hashAlgo.TransformBlock(buffer, offset, n, buffer, offset); + return n; + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + if (_finished) + throw new ObjectDisposedException("HashingStream"); + _stream.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (_finished) + throw new ObjectDisposedException("HashingStream"); + _stream.Write(buffer, offset, count); + string x = new(System.Text.Encoding.ASCII.GetChars(buffer.AsSpan().Slice(offset, count).ToArray())); + _hashAlgo.TransformBlock(buffer, offset, count, buffer, offset); + } + + public byte[] Finish() + { + if (_finished) + return _hashAlgo.Hash; + _hashAlgo.TransformFinalBlock(Array.Empty(), 0, 0); + if (DisposeUnderlying) + _stream.Dispose(); + return _hashAlgo.Hash; + } +} diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/RequestBlockFileListResult.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/RequestBlockFileListResult.cs new file mode 100644 index 0000000..303e287 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/RequestBlockFileListResult.cs @@ -0,0 +1,76 @@ +using MareSynchronosShared.Metrics; +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils.Configuration; +using MareSynchronosStaticFilesServer.Services; +using Microsoft.AspNetCore.Mvc; +using System.Globalization; +using System.Text; + +namespace MareSynchronosStaticFilesServer.Utils; + +public class RequestBlockFileListResult : IActionResult +{ + private readonly Guid _requestId; + private readonly RequestQueueService _requestQueueService; + private readonly MareMetrics _mareMetrics; + private readonly IEnumerable _fileList; + private readonly IConfigurationService _configurationService; + + public RequestBlockFileListResult(Guid requestId, RequestQueueService requestQueueService, MareMetrics mareMetrics, IEnumerable fileList, + IConfigurationService configurationService) + { + _requestId = requestId; + _requestQueueService = requestQueueService; + _mareMetrics = mareMetrics; + _mareMetrics.IncGauge(MetricsAPI.GaugeCurrentDownloads); + _fileList = fileList; + _configurationService = configurationService; + } + + public async Task ExecuteResultAsync(ActionContext context) + { + try + { + ArgumentNullException.ThrowIfNull(context); + + var useSSI = _configurationService.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseSSI), false); + + context.HttpContext.Response.StatusCode = 200; + + if (useSSI) + context.HttpContext.Response.ContentType = _configurationService.GetValue(nameof(StaticFilesServerConfiguration.SSIContentType)); + else + context.HttpContext.Response.ContentType = "application/octet-stream"; + + string ssiFilePrefix = null; + + if (useSSI) + ssiFilePrefix = _configurationService.GetValue(nameof(StaticFilesServerConfiguration.XAccelRedirectPrefix)); + + foreach (var file in _fileList) + { + if (useSSI) + { + var internalName = Path.Combine(ssiFilePrefix, file.Name); + await context.HttpContext.Response.WriteAsync( + "#" + file.Name + ":" + file.Length.ToString(CultureInfo.InvariantCulture) + "#" + + "", Encoding.ASCII); + } + else + { + await context.HttpContext.Response.WriteAsync("#" + file.Name + ":" + file.Length.ToString(CultureInfo.InvariantCulture) + "#", Encoding.ASCII); + await context.HttpContext.Response.SendFileAsync(file.FullName); + } + } + } + catch + { + throw; + } + finally + { + _requestQueueService.FinishRequest(_requestId); + _mareMetrics.DecGauge(MetricsAPI.GaugeCurrentDownloads); + } + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/RequestBlockFileListResultFactory.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/RequestBlockFileListResultFactory.cs new file mode 100644 index 0000000..b9103a6 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/RequestBlockFileListResultFactory.cs @@ -0,0 +1,25 @@ +using MareSynchronosShared.Metrics; +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils.Configuration; +using MareSynchronosStaticFilesServer.Services; + +namespace MareSynchronosStaticFilesServer.Utils; + +public class RequestBlockFileListResultFactory +{ + private readonly MareMetrics _metrics; + private readonly RequestQueueService _requestQueueService; + private readonly IConfigurationService _configurationService; + + public RequestBlockFileListResultFactory(MareMetrics metrics, RequestQueueService requestQueueService, IConfigurationService configurationService) + { + _metrics = metrics; + _requestQueueService = requestQueueService; + _configurationService = configurationService; + } + + public RequestBlockFileListResult Create(Guid requestId, IEnumerable fileList) + { + return new RequestBlockFileListResult(requestId, _requestQueueService, _metrics, fileList, _configurationService); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/TeeStream.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/TeeStream.cs new file mode 100644 index 0000000..aed9355 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/TeeStream.cs @@ -0,0 +1,74 @@ +namespace MareSynchronosStaticFilesServer.Utils; + +// Writes data read from one stream out to a second stream +public class TeeStream : Stream +{ + private readonly Stream _inStream; + private readonly Stream _outStream; + public bool DisposeUnderlying = true; + + public Stream InStream { get => _inStream; } + public Stream OutStream { get => _outStream; } + + public TeeStream(Stream inStream, Stream outStream) + { + _inStream = inStream; + _outStream = outStream; + } + + protected override void Dispose(bool disposing) + { + if (!DisposeUnderlying) + return; + _inStream.Dispose(); + _outStream.Dispose(); + } + + public override bool CanRead => _inStream.CanRead; + public override bool CanSeek => _inStream.CanSeek; + public override bool CanWrite => false; + public override long Length => _inStream.Length; + + public override long Position + { + get => _inStream.Position; + set => _inStream.Position = value; + } + + public override void Flush() + { + _inStream.Flush(); + _outStream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + int n = _inStream.Read(buffer, offset, count); + if (n > 0) + _outStream.Write(buffer, offset, n); + return n; + } + + public async override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + int n = await _inStream.ReadAsync(buffer, offset, count, cancellationToken); + if (n > 0) + await _outStream.WriteAsync(buffer, offset, n); + return n; + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _inStream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + _inStream.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } +} diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/UserQueueEntry.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/UserQueueEntry.cs new file mode 100644 index 0000000..5646eca --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/UserQueueEntry.cs @@ -0,0 +1,21 @@ +namespace MareSynchronosStaticFilesServer.Utils; + +public class UserQueueEntry +{ + public UserQueueEntry(UserRequest userRequest, DateTime expirationDate) + { + UserRequest = userRequest; + ExpirationDate = expirationDate; + } + + public void MarkActive() + { + IsActive = true; + ActivationDate = DateTime.UtcNow; + } + + public UserRequest UserRequest { get; } + public DateTime ExpirationDate { get; } + public bool IsActive { get; private set; } = false; + public DateTime ActivationDate { get; private set; } +} diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/UserRequest.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/UserRequest.cs new file mode 100644 index 0000000..f64def1 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Utils/UserRequest.cs @@ -0,0 +1,6 @@ +namespace MareSynchronosStaticFilesServer.Utils; + +public record UserRequest(Guid RequestId, string User, List FileIds) +{ + public bool IsCancelled { get; set; } = false; +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/appsettings.Development.json b/MareSynchronosServer/MareSynchronosStaticFilesServer/appsettings.Development.json new file mode 100644 index 0000000..770d3e9 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/appsettings.json b/MareSynchronosServer/MareSynchronosStaticFilesServer/appsettings.json new file mode 100644 index 0000000..0cade29 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/appsettings.json @@ -0,0 +1,31 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5432;Database=mare;Username=postgres" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://+:5001" + }, + "Gprc": { + "Protocols": "Http2", + "Url": "http://+:5003" + } + } + }, + "MareSynchronos": { + "ForcedDeletionOfFilesAfterHours": -1, + "CacheSizeHardLimitInGiB": -1, + "UnusedFileRetentionPeriodInDays": 7, + "CacheDirectory": "G:\\ServerTest", + "ServiceAddress": "http://localhost:5002", + "RemoteCacheSourceUri": "" + }, + "AllowedHosts": "*" +}