From 80235a174bf1140f14f22e34e9834e46467ec892 Mon Sep 17 00:00:00 2001 From: Zurazan Date: Wed, 27 Aug 2025 03:02:29 +0200 Subject: [PATCH] Initial --- .dockerignore | 284 +++++ .gitignore | 353 ++++++ .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 | 106 ++ 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 | 60 + .../standalone/services-standalone.json | 46 + 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 + LightlessAPI/.gitignore | 350 ++++++ LightlessAPI/LICENSE | 21 + .../LightlessSyncAPI/Data/CharacterData.cs | 37 + .../Data/Comparer/GroupDataComparer.cs | 22 + .../Data/Comparer/GroupDtoComparer.cs | 24 + .../Data/Comparer/GroupPairDtoComparer.cs | 24 + .../Data/Comparer/UserDataComparer.cs | 22 + .../Data/Comparer/UserDtoComparer.cs | 24 + .../LightlessSyncAPI/Data/Constants.cs | 6 + .../Data/Enum/GroupPairUserInfo.cs | 9 + .../Data/Enum/GroupPermissions.cs | 11 + .../Enum/GroupUserPreferredPermissions.cs | 11 + .../Data/Enum/IndividualPairStatus.cs | 8 + .../Data/Enum/MessageSeverity.cs | 8 + .../LightlessSyncAPI/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 | 29 + .../LightlessSyncAPI/Data/GroupData.cs | 10 + .../LightlessSyncAPI/Data/UserData.cs | 10 + .../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 + .../LightlessSyncAPI/Dto/ConnectionDto.cs | 27 + .../Dto/DefaultPermissionsDto.cs | 15 + .../Dto/Files/DownloadFileDto.cs | 15 + .../Dto/Files/FilesSendDto.cs | 13 + .../Dto/Files/ITransferFileDto.cs | 8 + .../Dto/Files/UploadFileDto.cs | 11 + .../Dto/Group/BannedGroupUserDto.cs | 19 + .../LightlessSyncAPI/Dto/Group/GroupDto.cs | 13 + .../Dto/Group/GroupFullInfoDto.cs | 14 + .../Dto/Group/GroupInfoDto.cs | 18 + .../Dto/Group/GroupJoinDto.cs | 11 + .../Dto/Group/GroupPairDto.cs | 12 + .../Dto/Group/GroupPairFullInfoDto.cs | 8 + .../Dto/Group/GroupPairUserInfoDto.cs | 8 + .../Dto/Group/GroupPairUserPermissionDto.cs | 8 + .../Dto/Group/GroupPermissionDto.cs | 8 + .../LightlessSyncAPI/Dto/SystemInfoDto.cs | 9 + .../Dto/User/BulkPermissionsDto.cs | 7 + .../Dto/User/CensusDataDto.cs | 6 + .../Dto/User/OnlineUserCharaDataDto.cs | 7 + .../Dto/User/OnlineUserIdentDto.cs | 7 + .../Dto/User/UserCharaDataMessageDto.cs | 7 + .../LightlessSyncAPI/Dto/User/UserDto.cs | 7 + .../Dto/User/UserIndividualPairStatusDto.cs | 8 + .../LightlessSyncAPI/Dto/User/UserPairDto.cs | 21 + .../Dto/User/UserPermissionsDto.cs | 8 + .../Dto/User/UserProfileDto.cs | 7 + .../LightlessSyncAPI/LightlessSync.API.csproj | 13 + .../LightlessSyncAPI/LightlessSyncAPI.sln | 25 + .../LightlessSyncAPI/Routes/LightlessAuth.cs | 23 + .../LightlessSyncAPI/Routes/LightlessFiles.cs | 47 + .../LightlessSyncAPI/SignalR/ILightlessHub.cs | 91 ++ .../SignalR/ILightlessHubClient.cs | 61 + .../Authentication/SecretKeyAuthReply.cs | 3 + .../SecretKeyFailedAuthorization.cs | 12 + .../Controllers/AuthControllerBase.cs | 160 +++ .../Controllers/JwtController.cs | 89 ++ .../Controllers/OAuthController.cs | 307 +++++ .../LightlessSyncAuthService.csproj | 22 + .../LightlessSyncAuthService/Program.cs | 55 + .../Properties/launchSettings.json | 29 + .../Services/GeoIPService.cs | 143 +++ .../Services/SecretKeyAuthenticatorService.cs | 132 ++ .../LightlessSyncAuthService/Startup.cs | 244 ++++ .../appsettings.Development.json | 8 + .../LightlessSyncAuthService/appsettings.json | 9 + LightlessSyncServer/LightlessSyncServer.sln | 65 + .../Controllers/ClientMessageController.cs | 42 + .../Hubs/ConcurrencyFilter.cs | 108 ++ .../Hubs/MareHub.CharaData.cs | 641 ++++++++++ .../Hubs/MareHub.ClientStubs.cs | 58 + .../Hubs/MareHub.Functions.cs | 488 ++++++++ .../Hubs/MareHub.GposeLobby.cs | 155 +++ .../Hubs/MareHub.Groups.cs | 672 ++++++++++ .../Hubs/MareHub.Permissions.cs | 172 +++ .../LightlessSyncServer/Hubs/MareHub.User.cs | 437 +++++++ .../LightlessSyncServer/Hubs/MareHub.cs | 212 ++++ .../Hubs/SignalRLimitFilter.cs | 112 ++ .../LightlessSyncServer.csproj | 41 + .../LightlessSyncServer/Program.cs | 96 ++ .../Properties/launchSettings.json | 14 + .../Properties/serviceDependencies.json | 3 + .../Properties/serviceDependencies.local.json | 3 + .../Services/CharaDataCleanupService.cs | 42 + .../ClientPairPermissionsCleanupService.cs | 190 +++ .../Services/GPoseLobbyDistributionService.cs | 226 ++++ .../Services/MareCensus.cs | 182 +++ .../Services/OnlineSyncedPairCacheService.cs | 145 +++ .../Services/SystemInfoService.cs | 84 ++ .../Services/UserCleanupService.cs | 194 +++ .../LightlessSyncServer/Startup.cs | 367 ++++++ .../LightlessSyncServer/Utils/Extensions.cs | 74 ++ .../Utils/MareHubLogger.cs | 34 + .../LightlessSyncServer/Utils/PauseInfo.cs | 8 + .../LightlessSyncServer/Utils/PauseState.cs | 9 + .../LightlessSyncServer/Utils/PausedEntry.cs | 58 + .../LightlessSyncServer/Utils/UserPair.cs | 12 + .../appsettings.Development.json | 10 + .../LightlessSyncServer/appsettings.json | 61 + .../Discord/DiscordBotTest.cs | 53 + .../Hubs/MareHubTest.cs | 82 ++ .../LightlessSyncServerTest.csproj | 26 + .../LightlessSyncServerTest/Usings.cs | 1 + .../Discord/DiscordBot.cs | 434 +++++++ .../Discord/DiscordBotServices.cs | 175 +++ .../Discord/LodestoneModal.cs | 15 + .../Discord/MareModule.cs | 293 +++++ .../MareWizardModule.AprilFools2024.cs | 215 ++++ .../Discord/MareWizardModule.Delete.cs | 119 ++ .../Discord/MareWizardModule.Recover.cs | 90 ++ .../Discord/MareWizardModule.Register.cs | 311 +++++ .../Discord/MareWizardModule.Relink.cs | 281 +++++ .../Discord/MareWizardModule.Secondary.cs | 91 ++ .../Discord/MareWizardModule.UserInfo.cs | 80 ++ .../Discord/MareWizardModule.Vanity.cs | 205 +++ .../Discord/MareWizardModule.cs | 346 ++++++ .../LightlessSyncServices.csproj | 42 + .../LightlessSyncServices/Program.cs | 57 + .../Properties/launchSettings.json | 13 + .../LightlessSyncServices/Startup.cs | 76 ++ .../appsettings.Development.json | 8 + .../LightlessSyncServices/appsettings.json | 29 + .../LightlessSyncShared/Data/MareDbContext.cs | 145 +++ .../LightlessSyncShared/Extensions.cs | 39 + .../LightlessSyncShared.csproj | 57 + .../Metrics/MareMetrics.cs | 84 ++ .../LightlessSyncShared/Metrics/MetricsAPI.cs | 53 + .../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 + .../20230924190113_permissions.Designer.cs | 805 ++++++++++++ .../Migrations/20230924190113_permissions.cs | 442 +++++++ ...0230926212023_AlterPermissions.Designer.cs | 868 +++++++++++++ .../20230926212023_AlterPermissions.cs | 87 ++ .../20231027090903_PausedIndex.Designer.cs | 807 ++++++++++++ .../Migrations/20231027090903_PausedIndex.cs | 27 + .../20240718095806_MarkForBan.Designer.cs | 811 ++++++++++++ .../Migrations/20240718095806_MarkForBan.cs | 309 +++++ .../20240718100453_RemoveReport.Designer.cs | 757 ++++++++++++ .../Migrations/20240718100453_RemoveReport.cs | 59 + .../20240904144711_AddRawFileSize.Designer.cs | 761 ++++++++++++ .../20240904144711_AddRawFileSize.cs | 29 + .../20241226112428_CharaData.Designer.cs | 1024 +++++++++++++++ .../Migrations/20241226112428_CharaData.cs | 198 +++ ...241226194944_CharaDataFileSwap.Designer.cs | 1064 ++++++++++++++++ .../20241226194944_CharaDataFileSwap.cs | 114 ++ .../20241227130901_CascadeFile.Designer.cs | 1065 ++++++++++++++++ .../Migrations/20241227130901_CascadeFile.cs | 41 + ...0241227190944_OrigFileGamePath.Designer.cs | 1069 ++++++++++++++++ .../20241227190944_OrigFileGamePath.cs | 65 + .../20241228190750_ManipData.Designer.cs | 1073 ++++++++++++++++ .../Migrations/20241228190750_ManipData.cs | 28 + .../20250112111727_AllowedGroup.Designer.cs | 1094 +++++++++++++++++ .../Migrations/20250112111727_AllowedGroup.cs | 98 ++ .../Migrations/MareDbContextModelSnapshot.cs | 1091 ++++++++++++++++ .../LightlessSyncShared/Models/Auth.cs | 17 + .../LightlessSyncShared/Models/Banned.cs | 13 + .../Models/BannedRegistrations.cs | 10 + .../LightlessSyncShared/Models/CharaData.cs | 91 ++ .../LightlessSyncShared/Models/ClientPair.cs | 15 + .../LightlessSyncShared/Models/FileCache.cs | 19 + .../Models/ForbiddenUploadEntry.cs | 14 + .../LightlessSyncShared/Models/Group.cs | 19 + .../LightlessSyncShared/Models/GroupBan.cs | 13 + .../LightlessSyncShared/Models/GroupPair.cs | 11 + .../Models/GroupPairPreferredPermission.cs | 13 + .../Models/GroupTempInvite.cs | 12 + .../Models/LodeStoneAuth.cs | 15 + .../LightlessSyncShared/Models/User.cs | 20 + .../Models/UserDefaultPreferredPermission.cs | 22 + .../Models/UserPermissionQuery.cs | 40 + .../Models/UserPermissionSet.cs | 18 + .../Models/UserProfileData.cs | 20 + .../ExistingUserRequirement.cs | 5 + .../ExistingUserRequirementHandler.cs | 84 ++ .../RedisDbUserRequirementHandler.cs | 54 + .../RequirementHandlers/UserRequirement.cs | 13 + .../UserRequirementHandler.cs | 75 ++ .../RequirementHandlers/UserRequirements.cs | 8 + .../ValidTokenHubRequirementHandler.cs | 54 + .../ValidTokenRequirement.cs | 5 + .../Services/IConfigurationService.cs | 14 + .../Services/MareConfigurationController.cs | 60 + .../MareConfigurationServiceClient.cs | 193 +++ .../MareConfigurationServiceServer.cs | 67 + .../AllowedControllersFeatureProvider.cs | 22 + .../Utils/ClientMessage.cs | 4 + .../Configuration/AuthServiceConfiguration.cs | 24 + .../Utils/Configuration/IMareConfiguration.cs | 8 + .../Configuration/MareConfigurationBase.cs | 51 + .../Configuration/ServerConfiguration.cs | 52 + .../Configuration/ServicesConfiguration.cs | 34 + .../Utils/Configuration/ShardConfiguration.cs | 8 + .../StaticFilesServerConfiguration.cs | 45 + .../Utils/IdBasedUserIdProvider.cs | 11 + .../Utils/MareClaimTypes.cs | 14 + .../Utils/RemoteConfigurationAttribute.cs | 4 + .../Utils/ServerTokenGenerator.cs | 65 + .../Utils/SharedDbFunctions.cs | 129 ++ .../LightlessSyncShared/Utils/StringUtils.cs | 49 + .../Controllers/CacheController.cs | 53 + .../Controllers/ControllerBase.cs | 18 + .../Controllers/DistributionController.cs | 29 + .../Controllers/MainController.cs | 66 + .../Controllers/RequestController.cs | 63 + .../Controllers/ServerFilesController.cs | 363 ++++++ .../Controllers/SpeedTestController.cs | 61 + .../DummyHub.cs | 25 + .../LightlessSyncStaticFilesServer.csproj | 38 + .../LightlessSyncStaticFilesServer/Program.cs | 63 + .../Properties/launchSettings.json | 28 + .../Services/CachedFileProvider.cs | 214 ++++ .../Services/FileStatisticsService.cs | 94 ++ .../Services/IClientReadyMessageService.cs | 6 + .../Services/MainClientReadyMessageService.cs | 23 + .../Services/MainFileCleanupService.cs | 367 ++++++ .../MainServerShardRegistrationService.cs | 96 ++ .../Services/RequestQueueService.cs | 228 ++++ .../ShardClientReadyMessageService.cs | 45 + .../Services/ShardFileCleanupService.cs | 163 +++ .../Services/ShardRegistrationService.cs | 107 ++ .../LightlessSyncStaticFilesServer/Startup.cs | 272 ++++ .../Utils/BlockFileDataStream.cs | 108 ++ .../Utils/BlockFileDataSubstream.cs | 115 ++ .../Utils/FilePathUtil.cs | 31 + .../Utils/RequestFileStreamResult.cs | 58 + .../Utils/RequestFileStreamResultFactory.cs | 26 + .../Utils/UserQueueEntry.cs | 21 + .../Utils/UserRequest.cs | 6 + .../appsettings.Development.json | 9 + .../appsettings.json | 31 + 344 files changed, 43249 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 LightlessAPI/.gitignore create mode 100644 LightlessAPI/LICENSE create mode 100644 LightlessAPI/LightlessSyncAPI/Data/CharacterData.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Data/Comparer/GroupDataComparer.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Data/Comparer/GroupDtoComparer.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Data/Comparer/GroupPairDtoComparer.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Data/Comparer/UserDataComparer.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Data/Comparer/UserDtoComparer.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Data/Constants.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Data/Enum/GroupPairUserInfo.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Data/Enum/GroupPermissions.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Data/Enum/GroupUserPreferredPermissions.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Data/Enum/IndividualPairStatus.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Data/Enum/MessageSeverity.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Data/Enum/ObjectKind.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Data/Enum/UserPermissions.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Data/Extensions/GroupPermissionsExtensions.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Data/Extensions/GroupUserInfoExtensions.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Data/Extensions/GroupUserPermissionsExtensions.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Data/Extensions/UserPermissionsExtensions.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Data/FileReplacementData.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Data/GroupData.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Data/UserData.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/CharaData/AccessTypeDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/CharaData/CharaDataDownloadDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/CharaData/CharaDataDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/CharaData/CharaDataFullDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/CharaData/CharaDataMetaInfoDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/CharaData/CharaDataUpdateDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/CharaData/ShareTypeDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/ConnectionDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/DefaultPermissionsDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/Files/DownloadFileDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/Files/FilesSendDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/Files/ITransferFileDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/Files/UploadFileDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/Group/BannedGroupUserDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/Group/GroupDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/Group/GroupFullInfoDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/Group/GroupInfoDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/Group/GroupJoinDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/Group/GroupPairDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/Group/GroupPairFullInfoDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/Group/GroupPairUserInfoDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/Group/GroupPairUserPermissionDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/Group/GroupPermissionDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/SystemInfoDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/User/BulkPermissionsDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/User/CensusDataDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/User/OnlineUserCharaDataDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/User/OnlineUserIdentDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/User/UserCharaDataMessageDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/User/UserDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/User/UserIndividualPairStatusDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/User/UserPairDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/User/UserPermissionsDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Dto/User/UserProfileDto.cs create mode 100644 LightlessAPI/LightlessSyncAPI/LightlessSync.API.csproj create mode 100644 LightlessAPI/LightlessSyncAPI/LightlessSyncAPI.sln create mode 100644 LightlessAPI/LightlessSyncAPI/Routes/LightlessAuth.cs create mode 100644 LightlessAPI/LightlessSyncAPI/Routes/LightlessFiles.cs create mode 100644 LightlessAPI/LightlessSyncAPI/SignalR/ILightlessHub.cs create mode 100644 LightlessAPI/LightlessSyncAPI/SignalR/ILightlessHubClient.cs create mode 100644 LightlessSyncServer/LightlessSyncAuthService/Authentication/SecretKeyAuthReply.cs create mode 100644 LightlessSyncServer/LightlessSyncAuthService/Authentication/SecretKeyFailedAuthorization.cs create mode 100644 LightlessSyncServer/LightlessSyncAuthService/Controllers/AuthControllerBase.cs create mode 100644 LightlessSyncServer/LightlessSyncAuthService/Controllers/JwtController.cs create mode 100644 LightlessSyncServer/LightlessSyncAuthService/Controllers/OAuthController.cs create mode 100644 LightlessSyncServer/LightlessSyncAuthService/LightlessSyncAuthService.csproj create mode 100644 LightlessSyncServer/LightlessSyncAuthService/Program.cs create mode 100644 LightlessSyncServer/LightlessSyncAuthService/Properties/launchSettings.json create mode 100644 LightlessSyncServer/LightlessSyncAuthService/Services/GeoIPService.cs create mode 100644 LightlessSyncServer/LightlessSyncAuthService/Services/SecretKeyAuthenticatorService.cs create mode 100644 LightlessSyncServer/LightlessSyncAuthService/Startup.cs create mode 100644 LightlessSyncServer/LightlessSyncAuthService/appsettings.Development.json create mode 100644 LightlessSyncServer/LightlessSyncAuthService/appsettings.json create mode 100644 LightlessSyncServer/LightlessSyncServer.sln create mode 100644 LightlessSyncServer/LightlessSyncServer/Controllers/ClientMessageController.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Hubs/ConcurrencyFilter.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.CharaData.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.ClientStubs.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Functions.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.GposeLobby.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Permissions.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.User.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Hubs/SignalRLimitFilter.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/LightlessSyncServer.csproj create mode 100644 LightlessSyncServer/LightlessSyncServer/Program.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Properties/launchSettings.json create mode 100644 LightlessSyncServer/LightlessSyncServer/Properties/serviceDependencies.json create mode 100644 LightlessSyncServer/LightlessSyncServer/Properties/serviceDependencies.local.json create mode 100644 LightlessSyncServer/LightlessSyncServer/Services/CharaDataCleanupService.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Services/ClientPairPermissionsCleanupService.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Services/GPoseLobbyDistributionService.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Services/MareCensus.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Services/OnlineSyncedPairCacheService.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Services/SystemInfoService.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Services/UserCleanupService.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Startup.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Utils/MareHubLogger.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Utils/PauseInfo.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Utils/PauseState.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Utils/PausedEntry.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Utils/UserPair.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/appsettings.Development.json create mode 100644 LightlessSyncServer/LightlessSyncServer/appsettings.json create mode 100644 LightlessSyncServer/LightlessSyncServerTest/Discord/DiscordBotTest.cs create mode 100644 LightlessSyncServer/LightlessSyncServerTest/Hubs/MareHubTest.cs create mode 100644 LightlessSyncServer/LightlessSyncServerTest/LightlessSyncServerTest.csproj create mode 100644 LightlessSyncServer/LightlessSyncServerTest/Usings.cs create mode 100644 LightlessSyncServer/LightlessSyncServices/Discord/DiscordBot.cs create mode 100644 LightlessSyncServer/LightlessSyncServices/Discord/DiscordBotServices.cs create mode 100644 LightlessSyncServer/LightlessSyncServices/Discord/LodestoneModal.cs create mode 100644 LightlessSyncServer/LightlessSyncServices/Discord/MareModule.cs create mode 100644 LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.AprilFools2024.cs create mode 100644 LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Delete.cs create mode 100644 LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Recover.cs create mode 100644 LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Register.cs create mode 100644 LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Relink.cs create mode 100644 LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Secondary.cs create mode 100644 LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.UserInfo.cs create mode 100644 LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Vanity.cs create mode 100644 LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.cs create mode 100644 LightlessSyncServer/LightlessSyncServices/LightlessSyncServices.csproj create mode 100644 LightlessSyncServer/LightlessSyncServices/Program.cs create mode 100644 LightlessSyncServer/LightlessSyncServices/Properties/launchSettings.json create mode 100644 LightlessSyncServer/LightlessSyncServices/Startup.cs create mode 100644 LightlessSyncServer/LightlessSyncServices/appsettings.Development.json create mode 100644 LightlessSyncServer/LightlessSyncServices/appsettings.json create mode 100644 LightlessSyncServer/LightlessSyncShared/Data/MareDbContext.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Extensions.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/LightlessSyncShared.csproj create mode 100644 LightlessSyncServer/LightlessSyncShared/Metrics/MareMetrics.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Metrics/MetricsAPI.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20220731210149_InitialCreate.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20220731210149_InitialCreate.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20220731211419_RenameLowerSnakeCase.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20220731211419_RenameLowerSnakeCase.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20220801121419_AddLodestoneAuth.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20220801121419_AddLodestoneAuth.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20220806103053_AddBannedRegistrations.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20220806103053_AddBannedRegistrations.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20220816170426_SetMaxLimitForStrings.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20220816170426_SetMaxLimitForStrings.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20220824225157_AddAlias.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20220824225157_AddAlias.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20220917115233_Groups.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20220917115233_Groups.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20220929150304_ChangeGidLength.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20220929150304_ChangeGidLength.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20221002105428_IsPinned.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20221002105428_IsPinned.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20221004125939_AdjustAliasLength.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20221004125939_AdjustAliasLength.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20221006115929_GroupModerator.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20221006115929_GroupModerator.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20221006122618_groupbans.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20221006122618_groupbans.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20221024141548_GroupTempInvite.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20221024141548_GroupTempInvite.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20221024181912_AdjustInviteLength.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20221024181912_AdjustInviteLength.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20221228033214_FileCacheSize.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20221228033214_FileCacheSize.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20230111092127_IsBannedForAuth.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20230111092127_IsBannedForAuth.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20230118184347_FilesUploadDate.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20230118184347_FilesUploadDate.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20230126163758_GroupPerms.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20230126163758_GroupPerms.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20230131193425_AddPrimaryUserToAuth.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20230131193425_AddPrimaryUserToAuth.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20230228001033_UserPerms.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20230228001033_UserPerms.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20230319015307_UserProfileData.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20230319015307_UserProfileData.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20230319114005_UserProfileReports.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20230319114005_UserProfileReports.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20230420075153_DisableVFX.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20230420075153_DisableVFX.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20230924190113_permissions.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20230924190113_permissions.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20230926212023_AlterPermissions.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20230926212023_AlterPermissions.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20231027090903_PausedIndex.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20231027090903_PausedIndex.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20240718095806_MarkForBan.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20240718095806_MarkForBan.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20240718100453_RemoveReport.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20240718100453_RemoveReport.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20240904144711_AddRawFileSize.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20240904144711_AddRawFileSize.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20241226112428_CharaData.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20241226112428_CharaData.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20241226194944_CharaDataFileSwap.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20241226194944_CharaDataFileSwap.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20241227130901_CascadeFile.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20241227130901_CascadeFile.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20241227190944_OrigFileGamePath.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20241227190944_OrigFileGamePath.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20241228190750_ManipData.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20241228190750_ManipData.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20250112111727_AllowedGroup.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20250112111727_AllowedGroup.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/MareDbContextModelSnapshot.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Models/Auth.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Models/Banned.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Models/BannedRegistrations.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Models/CharaData.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Models/ClientPair.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Models/FileCache.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Models/ForbiddenUploadEntry.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Models/Group.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Models/GroupBan.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Models/GroupPair.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Models/GroupPairPreferredPermission.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Models/GroupTempInvite.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Models/LodeStoneAuth.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Models/User.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Models/UserDefaultPreferredPermission.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Models/UserPermissionQuery.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Models/UserPermissionSet.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Models/UserProfileData.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/RequirementHandlers/ExistingUserRequirement.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/RequirementHandlers/ExistingUserRequirementHandler.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/RequirementHandlers/RedisDbUserRequirementHandler.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/RequirementHandlers/UserRequirement.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/RequirementHandlers/UserRequirementHandler.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/RequirementHandlers/UserRequirements.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/RequirementHandlers/ValidTokenHubRequirementHandler.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/RequirementHandlers/ValidTokenRequirement.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Services/IConfigurationService.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Services/MareConfigurationController.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Services/MareConfigurationServiceClient.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Services/MareConfigurationServiceServer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Utils/AllowedControllersFeatureProvider.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Utils/ClientMessage.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Utils/Configuration/AuthServiceConfiguration.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Utils/Configuration/IMareConfiguration.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Utils/Configuration/MareConfigurationBase.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Utils/Configuration/ServerConfiguration.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Utils/Configuration/ServicesConfiguration.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Utils/Configuration/ShardConfiguration.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Utils/Configuration/StaticFilesServerConfiguration.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Utils/IdBasedUserIdProvider.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Utils/MareClaimTypes.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Utils/RemoteConfigurationAttribute.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Utils/ServerTokenGenerator.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Utils/SharedDbFunctions.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Utils/StringUtils.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/CacheController.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/ControllerBase.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/DistributionController.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/MainController.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/RequestController.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/ServerFilesController.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/SpeedTestController.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/DummyHub.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/LightlessSyncStaticFilesServer.csproj create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Program.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Properties/launchSettings.json create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Services/CachedFileProvider.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Services/FileStatisticsService.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Services/IClientReadyMessageService.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Services/MainClientReadyMessageService.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Services/MainFileCleanupService.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Services/MainServerShardRegistrationService.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Services/RequestQueueService.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Services/ShardClientReadyMessageService.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Services/ShardFileCleanupService.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Services/ShardRegistrationService.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Startup.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/BlockFileDataStream.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/BlockFileDataSubstream.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/FilePathUtil.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/RequestFileStreamResult.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/RequestFileStreamResultFactory.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/UserQueueEntry.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/UserRequest.cs create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/appsettings.Development.json create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/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..d9cf9a4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,353 @@ +## 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/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..dd4161a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "LightlessAPI"] + path = LightlessAPI + url = https://github.com/Light-Public-Syncshells/LightlessAPI 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..4f67c45 --- /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=Debug \ + --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..45cbdd8 --- /dev/null +++ b/Docker/run/compose/mare-sharded.yml @@ -0,0 +1,145 @@ +services: + postgres: + image: postgres:latest + restart: always + environment: + POSTGRES_DB: + POSTGRES_USER: + POSTGRES_PASSWORD: + volumes: + - ../data/postgresql/:/var/lib/postgresql/data + - postgres_socket:/var/run/postgresql:rw + healthcheck: + test: ["CMD-SHELL", "pg_isready -U "] + 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..bce487b --- /dev/null +++ b/Docker/run/compose/mare-standalone.yml @@ -0,0 +1,106 @@ +services: + postgres: + image: postgres:latest + command: ["postgres", "-c", "log_statement=all"] + restart: always + ports: + - 5432:5432/tcp + environment: + POSTGRES_DB: -DB NAME HERE- + POSTGRES_USER: -USER HERE- + POSTGRES_PASSWORD: -PASS HERE- + volumes: + - ../data/postgresql/:/var/lib/postgresql/data + - postgres_socket:/var/run/postgresql:rw + healthcheck: + test: ["CMD-SHELL", "pg_isready -U -USER HERE-"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: redis:latest + command: [sh, -c, "rm -f /data/dump.rdb && redis-server --save \"\" --appendonly no --requirepass -PASS HERE-"] + volumes: + - cache:/data + + lightless-server: + image: lightless/server:local + restart: on-failure + ports: + - 6000:6000/tcp + - 6050:6050/tcp + environment: + LightlessSync__CdnFullUrl: "${DEV_LIGHTLESS_CDNURL}" + LightlessSync__XIVAPIKey: "${DEV_LIGHTLESS_XIVAPIKEY}" + DOTNET_USE_POLLING_FILE_WATCHER: 1 + volumes: + - ../config/standalone/server-standalone.json:/opt/LightlessSyncServer/appsettings.json + - ../log/server-standalone/:/opt/LightlessSyncServer/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 + + lightless-auth: + image: lightless/auth:local + restart: on-failure + environment: + DOTNET_USE_POLLING_FILE_WATCHER: 1 + volumes: + - ../config/standalone/authservice-standalone.json:/opt/LightlessSyncAuthService/appsettings.json + - ../log/authservice-standalone/:/opt/LightlessSyncAuthService/logs/:rw + - postgres_socket:/var/run/postgresql/:rw + depends_on: + lightless-server: + condition: service_healthy + postgres: + condition: service_healthy + + lightless-services: + image: lightless/services:local + restart: on-failure + environment: + LightlessSync__DiscordBotToken: "${DEV_LIGHTLESS_DISCORDTOKEN}" + LightlessSync__DiscordChannelForMessages: "${DEV_LIGHTLESS_DISCORDCHANNEL}" + LightlessSync__DiscordChannelForReports: "${DEV_LIGHTLESS_DISCORDCHANNEL}" + LightlessSync__DiscordChannelForCommands: "${DEV_LIGHTLESS_DISCORDCHANNEL}" + DOTNET_USE_POLLING_FILE_WATCHER: 1 + volumes: + - ../config/standalone/services-standalone.json:/opt/LightlessSyncServices/appsettings.json + - ../log/services-standalone/:/opt/LightlessSyncServices/logs/:rw + - postgres_socket:/var/run/postgresql/:rw + depends_on: + lightless-server: + condition: service_healthy + postgres: + condition: service_healthy + + lightless-files: + image: lightless/files:local + ports: + - 6200:6200/tcp + restart: on-failure + environment: + LightlessSync__CdnFullUrl: "${DEV_LIGHTLESS_CDNURL}" + DOTNET_USE_POLLING_FILE_WATCHER: 1 + volumes: + - ../config/standalone/files-standalone.json:/opt/LightlessSyncStaticFilesServer/appsettings.json + - ../log/files-standalone/:/opt/LightlessSyncStaticFilesServer/logs/:rw + - postgres_socket:/var/run/postgresql/:rw + - ../data/files-standalone/:/lightlesscache/:rw + depends_on: + postgres: + condition: service_healthy + lightless-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..a598d2e --- /dev/null +++ b/Docker/run/config/standalone/authservice-standalone.json @@ -0,0 +1,42 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=/var/run/postgresql;Port=5432;Database=;Username=;Password=;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", + "LightlessSyncServices": "Information", + "LightlessSyncShared": "Information", + "System.IO": "Information" + }, + "File": { + "BasePath": "logs", + "FileAccessMode": "KeepOpenAndAutoFlush", + "FileEncodingName": "utf-8", + "DateFormat": "yyyMMdd", + "MaxFileSize": 104857600, + "Files": [ + { + "Path": "///lightless--.log" + } + ] + } + }, + "LightlessSync": { + "DbContextPoolSize": 512, + "ShardName": "AuthServices", + "MetricsPort": 6150, + "Jwt": "", + "RedisConnectionString": "redis:6379,password=", + "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..10cfd88 --- /dev/null +++ b/Docker/run/config/standalone/files-standalone.json @@ -0,0 +1,53 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=/var/run/postgresql;Port=5432;Database=;Username=;Password=;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", + "LightlessSyncStaticFilesServer": "Debug", + "LightlessSyncShared": "Debug", + "System.IO": "Information", + }, + "File": { + "BasePath": "logs", + "FileAccessMode": "KeepOpenAndAutoFlush", + "FileEncodingName": "utf-8", + "DateFormat": "yyyMMdd", + "MaxFileSize": 104857600, + "Files": [ + { + "Path": "///lightless--.log" + } + ] + } + }, + "LightlessSync": { + "DbContextPoolSize": 512, + "ShardName": "Files", + "MetricsPort": 6250, + "ForcedDeletionOfFilesAfterHours": -1, + "CacheSizeHardLimitInGiB": -1, + "UnusedFileRetentionPeriodInDays": 14, + "CacheDirectory": "/lightlesscache/", + "RemoteCacheSourceUri": "", + "MainServerAddress": "http://-IPorDomainhere-:6000/", + "RedisConnectionString": "redis:6379,password=", + "MainFileServerAddress": "", + "Jwt": "", + "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..c959697 --- /dev/null +++ b/Docker/run/config/standalone/server-standalone.json @@ -0,0 +1,60 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=/var/run/postgresql;Port=5432;Database=;Username=;Password=;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", + "Microsoft.EntityFramework": "Warning", + "LightlessSyncServer": "Debug", + "LightlessSyncShared": "Debug", + "System.IO": "Information", + "LightlessSyncServer.Services.SystemInfoService": "Warning" + }, + "File": { + "BasePath": "logs", + "FileAccessMode": "KeepOpenAndAutoFlush", + "FileEncodingName": "utf-8", + "DateFormat": "yyyMMdd", + "MaxFileSize": 104857600, + "Files": [ + { + "Path": "///lightless--.log" + } + ] + } + }, + "LightlessSync": { + "DbContextPoolSize": 512, + "ShardName": "Main", + "MetricsPort": 6050, + "MainServerAddress": "", + "FailedAuthForTempBan": 5, + "TempBanDurationInMinutes": 5, + "Jwt": "", + "WhitelistedIps": [ + "" + ], + "RedisConnectionString": "redis:6379,password=", + "CdnFullUrl": "http://localhost:6200", + "MaxExistingGroupsByUser": 12, + "MaxJoinedGroupsByUser": 20, + "MaxGroupUserCount": 100, + "PurgeUnusedAccounts": false, + "PurgeUnusedAccountsPeriodInDays": 14, + "ExpectedClientVersion": "0.9.0", + "XIVAPIKey": "" + }, + "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..304341d --- /dev/null +++ b/Docker/run/config/standalone/services-standalone.json @@ -0,0 +1,46 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=/var/run/postgresql;Port=5432;Database=;Username=;Password=;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", + "LightlessSyncServices": "Information", + "LightlessSyncShared": "Information", + "System.IO": "Information" + }, + "File": { + "BasePath": "logs", + "FileAccessMode": "KeepOpenAndAutoFlush", + "FileEncodingName": "utf-8", + "DateFormat": "yyyMMdd", + "MaxFileSize": 104857600, + "Files": [ + { + "Path": "///lightless--.log" + } + ] + } + }, + "LightlessSync": { + "DbContextPoolSize": 512, + "ShardName": "Services", + "MetricsPort": 6150, + "CdnFullUrl": "http://localhost:6200/", + "MainServerAddress": "http://-IPorDomainHere-:6000/", + "MainServerGrpcAddress": "http://-IPorDomainHere-:6005/", + "DiscordBotToken": "", + "DiscordChannelForMessages": "", + "DiscordChannelForCommands": "", + "Jwt": "", + "RedisConnectionString": "redis:6379,password=", + "VanityRoles": {"1408568079394537684":"Lightless Developer", "1349429584688578584":"Mods", "1401582542238388355":"Patreon", "1409255218591432817":"Kofi", "1409253125952507904":"Pure"} + }, + "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/LightlessAPI/.gitignore b/LightlessAPI/.gitignore new file mode 100644 index 0000000..9f298ec --- /dev/null +++ b/LightlessAPI/.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/ \ No newline at end of file diff --git a/LightlessAPI/LICENSE b/LightlessAPI/LICENSE new file mode 100644 index 0000000..f0d7bf5 --- /dev/null +++ b/LightlessAPI/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/LightlessAPI/LightlessSyncAPI/Data/CharacterData.cs b/LightlessAPI/LightlessSyncAPI/Data/CharacterData.cs new file mode 100644 index 0000000..42fcf1d --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Data/CharacterData.cs @@ -0,0 +1,37 @@ +using LightlessSync.API.Data.Enum; +using MessagePack; +using System.Security.Cryptography; + +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace LightlessSync.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/LightlessAPI/LightlessSyncAPI/Data/Comparer/GroupDataComparer.cs b/LightlessAPI/LightlessSyncAPI/Data/Comparer/GroupDataComparer.cs new file mode 100644 index 0000000..95d0881 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Data/Comparer/GroupDataComparer.cs @@ -0,0 +1,22 @@ +namespace LightlessSync.API.Data.Comparer; + +public class GroupDataComparer : IEqualityComparer +{ + private static GroupDataComparer _instance = new GroupDataComparer(); + + private GroupDataComparer() + { } + + public static GroupDataComparer Instance => _instance; + + 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(); + } +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Data/Comparer/GroupDtoComparer.cs b/LightlessAPI/LightlessSyncAPI/Data/Comparer/GroupDtoComparer.cs new file mode 100644 index 0000000..8ffa4b0 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Data/Comparer/GroupDtoComparer.cs @@ -0,0 +1,24 @@ +using LightlessSync.API.Dto.Group; + +namespace LightlessSync.API.Data.Comparer; + +public class GroupDtoComparer : IEqualityComparer +{ + private static GroupDtoComparer _instance = new GroupDtoComparer(); + + private GroupDtoComparer() + { } + + public static GroupDtoComparer Instance => _instance; + + 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/LightlessAPI/LightlessSyncAPI/Data/Comparer/GroupPairDtoComparer.cs b/LightlessAPI/LightlessSyncAPI/Data/Comparer/GroupPairDtoComparer.cs new file mode 100644 index 0000000..46fb844 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Data/Comparer/GroupPairDtoComparer.cs @@ -0,0 +1,24 @@ +using LightlessSync.API.Dto.Group; + +namespace LightlessSync.API.Data.Comparer; + +public class GroupPairDtoComparer : IEqualityComparer +{ + private static GroupPairDtoComparer _instance = new(); + + private GroupPairDtoComparer() + { } + + public static GroupPairDtoComparer Instance => _instance; + + 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/LightlessAPI/LightlessSyncAPI/Data/Comparer/UserDataComparer.cs b/LightlessAPI/LightlessSyncAPI/Data/Comparer/UserDataComparer.cs new file mode 100644 index 0000000..0fc14b4 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Data/Comparer/UserDataComparer.cs @@ -0,0 +1,22 @@ +namespace LightlessSync.API.Data.Comparer; + +public class UserDataComparer : IEqualityComparer +{ + private static UserDataComparer _instance = new(); + + private UserDataComparer() + { } + + public static UserDataComparer Instance => _instance; + + 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(); + } +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Data/Comparer/UserDtoComparer.cs b/LightlessAPI/LightlessSyncAPI/Data/Comparer/UserDtoComparer.cs new file mode 100644 index 0000000..4c0d2a4 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Data/Comparer/UserDtoComparer.cs @@ -0,0 +1,24 @@ +using LightlessSync.API.Dto.User; + +namespace LightlessSync.API.Data.Comparer; + +public class UserDtoComparer : IEqualityComparer +{ + private static UserDtoComparer _instance = new(); + + private UserDtoComparer() + { } + + public static UserDtoComparer Instance => _instance; + + 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/LightlessAPI/LightlessSyncAPI/Data/Constants.cs b/LightlessAPI/LightlessSyncAPI/Data/Constants.cs new file mode 100644 index 0000000..ad95fe8 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Data/Constants.cs @@ -0,0 +1,6 @@ +namespace LightlessSync.API.Data; + +public class Constants +{ + public const string IndividualKeyword = "//LIGHTLESS//DIRECT"; +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Data/Enum/GroupPairUserInfo.cs b/LightlessAPI/LightlessSyncAPI/Data/Enum/GroupPairUserInfo.cs new file mode 100644 index 0000000..3d17685 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Data/Enum/GroupPairUserInfo.cs @@ -0,0 +1,9 @@ +namespace LightlessSync.API.Data.Enum; + +[Flags] +public enum GroupPairUserInfo +{ + None = 0x0, + IsModerator = 0x2, + IsPinned = 0x4 +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Data/Enum/GroupPermissions.cs b/LightlessAPI/LightlessSyncAPI/Data/Enum/GroupPermissions.cs new file mode 100644 index 0000000..6a5b583 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Data/Enum/GroupPermissions.cs @@ -0,0 +1,11 @@ +namespace LightlessSync.API.Data.Enum; + +[Flags] +public enum GroupPermissions +{ + NoneSet = 0x0, + PreferDisableAnimations = 0x1, + PreferDisableSounds = 0x2, + DisableInvites = 0x4, + PreferDisableVFX = 0x8, +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Data/Enum/GroupUserPreferredPermissions.cs b/LightlessAPI/LightlessSyncAPI/Data/Enum/GroupUserPreferredPermissions.cs new file mode 100644 index 0000000..c3fe35d --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Data/Enum/GroupUserPreferredPermissions.cs @@ -0,0 +1,11 @@ +namespace LightlessSync.API.Data.Enum; + +[Flags] +public enum GroupUserPreferredPermissions +{ + NoneSet = 0x0, + Paused = 0x1, + DisableAnimations = 0x2, + DisableSounds = 0x4, + DisableVFX = 0x8, +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Data/Enum/IndividualPairStatus.cs b/LightlessAPI/LightlessSyncAPI/Data/Enum/IndividualPairStatus.cs new file mode 100644 index 0000000..bac0fbe --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Data/Enum/IndividualPairStatus.cs @@ -0,0 +1,8 @@ +namespace LightlessSync.API.Data.Enum; + +public enum IndividualPairStatus +{ + None, + OneSided, + Bidirectional +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Data/Enum/MessageSeverity.cs b/LightlessAPI/LightlessSyncAPI/Data/Enum/MessageSeverity.cs new file mode 100644 index 0000000..28fd22f --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Data/Enum/MessageSeverity.cs @@ -0,0 +1,8 @@ +namespace LightlessSync.API.Data.Enum; + +public enum MessageSeverity +{ + Information, + Warning, + Error +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Data/Enum/ObjectKind.cs b/LightlessAPI/LightlessSyncAPI/Data/Enum/ObjectKind.cs new file mode 100644 index 0000000..f044bc0 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Data/Enum/ObjectKind.cs @@ -0,0 +1,9 @@ +namespace LightlessSync.API.Data.Enum; + +public enum ObjectKind +{ + Player = 0, + MinionOrMount = 1, + Companion = 2, + Pet = 3, +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Data/Enum/UserPermissions.cs b/LightlessAPI/LightlessSyncAPI/Data/Enum/UserPermissions.cs new file mode 100644 index 0000000..4d2ae0c --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Data/Enum/UserPermissions.cs @@ -0,0 +1,12 @@ +namespace LightlessSync.API.Data.Enum; + +[Flags] +public enum UserPermissions +{ + NoneSet = 0, + Paused = 1, + DisableAnimations = 2, + DisableSounds = 4, + DisableVFX = 8, + Sticky = 16, +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Data/Extensions/GroupPermissionsExtensions.cs b/LightlessAPI/LightlessSyncAPI/Data/Extensions/GroupPermissionsExtensions.cs new file mode 100644 index 0000000..5b66f63 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Data/Extensions/GroupPermissionsExtensions.cs @@ -0,0 +1,50 @@ +using LightlessSync.API.Data.Enum; + +namespace LightlessSync.API.Data.Extensions; + +public static class GroupPermissionsExtensions +{ + public static bool IsDisableInvites(this GroupPermissions perm) + { + return perm.HasFlag(GroupPermissions.DisableInvites); + } + + public static bool IsPreferDisableAnimations(this GroupPermissions perm) + { + return perm.HasFlag(GroupPermissions.PreferDisableAnimations); + } + + public static bool IsPreferDisableSounds(this GroupPermissions perm) + { + return perm.HasFlag(GroupPermissions.PreferDisableSounds); + } + + public static bool IsPreferDisableVFX(this GroupPermissions perm) + { + return perm.HasFlag(GroupPermissions.PreferDisableVFX); + } + + public static void SetDisableInvites(this ref GroupPermissions perm, bool set) + { + if (set) perm |= GroupPermissions.DisableInvites; + else perm &= ~GroupPermissions.DisableInvites; + } + + public static void SetPreferDisableAnimations(this ref GroupPermissions perm, bool set) + { + if (set) perm |= GroupPermissions.PreferDisableAnimations; + else perm &= ~GroupPermissions.PreferDisableAnimations; + } + + public static void SetPreferDisableSounds(this ref GroupPermissions perm, bool set) + { + if (set) perm |= GroupPermissions.PreferDisableSounds; + else perm &= ~GroupPermissions.PreferDisableSounds; + } + + public static void SetPreferDisableVFX(this ref GroupPermissions perm, bool set) + { + if (set) perm |= GroupPermissions.PreferDisableVFX; + else perm &= ~GroupPermissions.PreferDisableVFX; + } +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Data/Extensions/GroupUserInfoExtensions.cs b/LightlessAPI/LightlessSyncAPI/Data/Extensions/GroupUserInfoExtensions.cs new file mode 100644 index 0000000..40e9b38 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Data/Extensions/GroupUserInfoExtensions.cs @@ -0,0 +1,28 @@ +using LightlessSync.API.Data.Enum; + +namespace LightlessSync.API.Data.Extensions; + +public static class GroupUserInfoExtensions +{ + public static bool IsModerator(this GroupPairUserInfo info) + { + return info.HasFlag(GroupPairUserInfo.IsModerator); + } + + public static bool IsPinned(this GroupPairUserInfo info) + { + return info.HasFlag(GroupPairUserInfo.IsPinned); + } + + public static void SetModerator(this ref GroupPairUserInfo info, bool isModerator) + { + if (isModerator) info |= GroupPairUserInfo.IsModerator; + else info &= ~GroupPairUserInfo.IsModerator; + } + + public static void SetPinned(this ref GroupPairUserInfo info, bool isPinned) + { + if (isPinned) info |= GroupPairUserInfo.IsPinned; + else info &= ~GroupPairUserInfo.IsPinned; + } +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Data/Extensions/GroupUserPermissionsExtensions.cs b/LightlessAPI/LightlessSyncAPI/Data/Extensions/GroupUserPermissionsExtensions.cs new file mode 100644 index 0000000..3ade66f --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Data/Extensions/GroupUserPermissionsExtensions.cs @@ -0,0 +1,50 @@ +using LightlessSync.API.Data.Enum; + +namespace LightlessSync.API.Data.Extensions; + +public static class GroupUserPermissionsExtensions +{ + public static bool IsDisableAnimations(this GroupUserPreferredPermissions perm) + { + return perm.HasFlag(GroupUserPreferredPermissions.DisableAnimations); + } + + public static bool IsDisableSounds(this GroupUserPreferredPermissions perm) + { + return perm.HasFlag(GroupUserPreferredPermissions.DisableSounds); + } + + public static bool IsDisableVFX(this GroupUserPreferredPermissions perm) + { + return perm.HasFlag(GroupUserPreferredPermissions.DisableVFX); + } + + public static bool IsPaused(this GroupUserPreferredPermissions perm) + { + return perm.HasFlag(GroupUserPreferredPermissions.Paused); + } + + public static void SetDisableAnimations(this ref GroupUserPreferredPermissions perm, bool set) + { + if (set) perm |= GroupUserPreferredPermissions.DisableAnimations; + else perm &= ~GroupUserPreferredPermissions.DisableAnimations; + } + + public static void SetDisableSounds(this ref GroupUserPreferredPermissions perm, bool set) + { + if (set) perm |= GroupUserPreferredPermissions.DisableSounds; + else perm &= ~GroupUserPreferredPermissions.DisableSounds; + } + + public static void SetDisableVFX(this ref GroupUserPreferredPermissions perm, bool set) + { + if (set) perm |= GroupUserPreferredPermissions.DisableVFX; + else perm &= ~GroupUserPreferredPermissions.DisableVFX; + } + + public static void SetPaused(this ref GroupUserPreferredPermissions perm, bool set) + { + if (set) perm |= GroupUserPreferredPermissions.Paused; + else perm &= ~GroupUserPreferredPermissions.Paused; + } +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Data/Extensions/UserPermissionsExtensions.cs b/LightlessAPI/LightlessSyncAPI/Data/Extensions/UserPermissionsExtensions.cs new file mode 100644 index 0000000..d4ae70f --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Data/Extensions/UserPermissionsExtensions.cs @@ -0,0 +1,61 @@ +using LightlessSync.API.Data.Enum; + +namespace LightlessSync.API.Data.Extensions; + +public static class UserPermissionsExtensions +{ + 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 bool IsPaused(this UserPermissions perm) + { + return perm.HasFlag(UserPermissions.Paused); + } + + public static bool IsSticky(this UserPermissions perm) + { + return perm.HasFlag(UserPermissions.Sticky); + } + + 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; + } + + public static void SetPaused(this ref UserPermissions perm, bool paused) + { + if (paused) perm |= UserPermissions.Paused; + else perm &= ~UserPermissions.Paused; + } + + public static void SetSticky(this ref UserPermissions perm, bool sticky) + { + if (sticky) perm |= UserPermissions.Sticky; + else perm &= ~UserPermissions.Sticky; + } +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Data/FileReplacementData.cs b/LightlessAPI/LightlessSyncAPI/Data/FileReplacementData.cs new file mode 100644 index 0000000..1acfb2f --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Data/FileReplacementData.cs @@ -0,0 +1,29 @@ +using MessagePack; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace LightlessSync.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 FileSwapPath { get; set; } = string.Empty; + public string[] GamePaths { get; set; } = Array.Empty(); + public string Hash { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Data/GroupData.cs b/LightlessAPI/LightlessSyncAPI/Data/GroupData.cs new file mode 100644 index 0000000..1a9abea --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Data/GroupData.cs @@ -0,0 +1,10 @@ +using MessagePack; + +namespace LightlessSync.API.Data; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupData(string GID, string? Alias = null) +{ + [IgnoreMember] + public string AliasOrGID => string.IsNullOrWhiteSpace(Alias) ? GID : Alias; +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Data/UserData.cs b/LightlessAPI/LightlessSyncAPI/Data/UserData.cs new file mode 100644 index 0000000..1ca6934 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Data/UserData.cs @@ -0,0 +1,10 @@ +using MessagePack; + +namespace LightlessSync.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/LightlessAPI/LightlessSyncAPI/Dto/CharaData/AccessTypeDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/CharaData/AccessTypeDto.cs new file mode 100644 index 0000000..826cdf5 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/CharaData/AccessTypeDto.cs @@ -0,0 +1,9 @@ +namespace LightlessSync.API.Dto.CharaData; + +public enum AccessTypeDto +{ + Individuals, + ClosePairs, + AllPairs, + Public +} diff --git a/LightlessAPI/LightlessSyncAPI/Dto/CharaData/CharaDataDownloadDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/CharaData/CharaDataDownloadDto.cs new file mode 100644 index 0000000..dab274c --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/CharaData/CharaDataDownloadDto.cs @@ -0,0 +1,14 @@ +using LightlessSync.API.Data; +using MessagePack; + +namespace LightlessSync.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/LightlessAPI/LightlessSyncAPI/Dto/CharaData/CharaDataDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/CharaData/CharaDataDto.cs new file mode 100644 index 0000000..68f7786 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/CharaData/CharaDataDto.cs @@ -0,0 +1,9 @@ +using LightlessSync.API.Data; + +namespace LightlessSync.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/LightlessAPI/LightlessSyncAPI/Dto/CharaData/CharaDataFullDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/CharaData/CharaDataFullDto.cs new file mode 100644 index 0000000..5f0bc2b --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/CharaData/CharaDataFullDto.cs @@ -0,0 +1,88 @@ +using LightlessSync.API.Data; +using MessagePack; + +namespace LightlessSync.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/LightlessAPI/LightlessSyncAPI/Dto/CharaData/CharaDataMetaInfoDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/CharaData/CharaDataMetaInfoDto.cs new file mode 100644 index 0000000..a952631 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/CharaData/CharaDataMetaInfoDto.cs @@ -0,0 +1,11 @@ +using LightlessSync.API.Data; +using MessagePack; + +namespace LightlessSync.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/LightlessAPI/LightlessSyncAPI/Dto/CharaData/CharaDataUpdateDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/CharaData/CharaDataUpdateDto.cs new file mode 100644 index 0000000..97f5143 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/CharaData/CharaDataUpdateDto.cs @@ -0,0 +1,20 @@ +using MessagePack; + +namespace LightlessSync.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/LightlessAPI/LightlessSyncAPI/Dto/CharaData/ShareTypeDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/CharaData/ShareTypeDto.cs new file mode 100644 index 0000000..2d199c2 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/CharaData/ShareTypeDto.cs @@ -0,0 +1,7 @@ +namespace LightlessSync.API.Dto.CharaData; + +public enum ShareTypeDto +{ + Private, + Shared +} diff --git a/LightlessAPI/LightlessSyncAPI/Dto/ConnectionDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/ConnectionDto.cs new file mode 100644 index 0000000..98eac82 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/ConnectionDto.cs @@ -0,0 +1,27 @@ +using LightlessSync.API.Data; +using MessagePack; + +namespace LightlessSync.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(); + public DefaultPermissionsDto DefaultPreferredPermissions { 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; } + public int MaxCharaDataVanity { get; set; } +} diff --git a/LightlessAPI/LightlessSyncAPI/Dto/DefaultPermissionsDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/DefaultPermissionsDto.cs new file mode 100644 index 0000000..90f69f0 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/DefaultPermissionsDto.cs @@ -0,0 +1,15 @@ +using MessagePack; + +namespace LightlessSync.API.Dto; + +[MessagePackObject(keyAsPropertyName: true)] +public record DefaultPermissionsDto +{ + public bool DisableIndividualAnimations { get; set; } = false; + public bool DisableIndividualSounds { get; set; } = false; + public bool DisableIndividualVFX { get; set; } = false; + public bool DisableGroupAnimations { get; set; } = false; + public bool DisableGroupSounds { get; set; } = false; + public bool DisableGroupVFX { get; set; } = false; + public bool IndividualIsSticky { get; set; } = true; +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Dto/Files/DownloadFileDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/Files/DownloadFileDto.cs new file mode 100644 index 0000000..2e8f8d4 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/Files/DownloadFileDto.cs @@ -0,0 +1,15 @@ +using MessagePack; + +namespace LightlessSync.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; + public long RawSize { get; set; } = 0; +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Dto/Files/FilesSendDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/Files/FilesSendDto.cs new file mode 100644 index 0000000..8c0e87f --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/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 LightlessSync.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/LightlessAPI/LightlessSyncAPI/Dto/Files/ITransferFileDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/Files/ITransferFileDto.cs new file mode 100644 index 0000000..d164f7a --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/Files/ITransferFileDto.cs @@ -0,0 +1,8 @@ +namespace LightlessSync.API.Dto.Files; + +public interface ITransferFileDto +{ + string ForbiddenBy { get; set; } + string Hash { get; set; } + bool IsForbidden { get; set; } +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Dto/Files/UploadFileDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/Files/UploadFileDto.cs new file mode 100644 index 0000000..f7f7c48 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/Files/UploadFileDto.cs @@ -0,0 +1,11 @@ +using MessagePack; + +namespace LightlessSync.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/LightlessAPI/LightlessSyncAPI/Dto/Group/BannedGroupUserDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/Group/BannedGroupUserDto.cs new file mode 100644 index 0000000..cceffe6 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/Group/BannedGroupUserDto.cs @@ -0,0 +1,19 @@ +using LightlessSync.API.Data; +using MessagePack; + +namespace LightlessSync.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/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupDto.cs new file mode 100644 index 0000000..eefb699 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupDto.cs @@ -0,0 +1,13 @@ +using LightlessSync.API.Data; +using MessagePack; + +namespace LightlessSync.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/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupFullInfoDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupFullInfoDto.cs new file mode 100644 index 0000000..68105ba --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupFullInfoDto.cs @@ -0,0 +1,14 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Data.Enum; +using MessagePack; + +namespace LightlessSync.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupFullInfoDto(GroupData Group, UserData Owner, GroupPermissions GroupPermissions, + GroupUserPreferredPermissions GroupUserPermissions, GroupPairUserInfo GroupUserInfo, + Dictionary GroupPairUserInfos) : GroupInfoDto(Group, Owner, GroupPermissions) +{ + public GroupUserPreferredPermissions GroupUserPermissions { get; set; } = GroupUserPermissions; + public GroupPairUserInfo GroupUserInfo { get; set; } = GroupUserInfo; +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupInfoDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupInfoDto.cs new file mode 100644 index 0000000..2e47955 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupInfoDto.cs @@ -0,0 +1,18 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Data.Enum; +using MessagePack; + +namespace LightlessSync.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; +} + +public record GroupJoinInfoDto(GroupData Group, UserData Owner, GroupPermissions GroupPermissions, bool Success) : GroupInfoDto(Group, Owner, GroupPermissions); \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupJoinDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupJoinDto.cs new file mode 100644 index 0000000..a440658 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupJoinDto.cs @@ -0,0 +1,11 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Data.Enum; +using MessagePack; + +namespace LightlessSync.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupPasswordDto(GroupData Group, string Password) : GroupDto(Group); + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupJoinDto(GroupData Group, string Password, GroupUserPreferredPermissions GroupUserPreferredPermissions) : GroupPasswordDto(Group, Password); \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupPairDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupPairDto.cs new file mode 100644 index 0000000..8dd903a --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupPairDto.cs @@ -0,0 +1,12 @@ +using LightlessSync.API.Data; +using MessagePack; + +namespace LightlessSync.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; +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupPairFullInfoDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupPairFullInfoDto.cs new file mode 100644 index 0000000..1c9b883 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupPairFullInfoDto.cs @@ -0,0 +1,8 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Data.Enum; +using MessagePack; + +namespace LightlessSync.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupPairFullInfoDto(GroupData Group, UserData User, UserPermissions SelfToOtherPermissions, UserPermissions OtherToSelfPermissions) : GroupPairDto(Group, User); \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupPairUserInfoDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupPairUserInfoDto.cs new file mode 100644 index 0000000..8b21137 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupPairUserInfoDto.cs @@ -0,0 +1,8 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Data.Enum; +using MessagePack; + +namespace LightlessSync.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupPairUserInfoDto(GroupData Group, UserData User, GroupPairUserInfo GroupUserInfo) : GroupPairDto(Group, User); \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupPairUserPermissionDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupPairUserPermissionDto.cs new file mode 100644 index 0000000..50d0947 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupPairUserPermissionDto.cs @@ -0,0 +1,8 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Data.Enum; +using MessagePack; + +namespace LightlessSync.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupPairUserPermissionDto(GroupData Group, UserData User, GroupUserPreferredPermissions GroupPairPermissions) : GroupPairDto(Group, User); \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupPermissionDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupPermissionDto.cs new file mode 100644 index 0000000..204e4c9 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/Group/GroupPermissionDto.cs @@ -0,0 +1,8 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Data.Enum; +using MessagePack; + +namespace LightlessSync.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupPermissionDto(GroupData Group, GroupPermissions Permissions) : GroupDto(Group); \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Dto/SystemInfoDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/SystemInfoDto.cs new file mode 100644 index 0000000..2eec666 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/SystemInfoDto.cs @@ -0,0 +1,9 @@ +using MessagePack; + +namespace LightlessSync.API.Dto; + +[MessagePackObject(keyAsPropertyName: true)] +public record SystemInfoDto +{ + public int OnlineUsers { get; set; } +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Dto/User/BulkPermissionsDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/User/BulkPermissionsDto.cs new file mode 100644 index 0000000..a3cc554 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/User/BulkPermissionsDto.cs @@ -0,0 +1,7 @@ +using LightlessSync.API.Data.Enum; +using MessagePack; + +namespace LightlessSync.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record BulkPermissionsDto(Dictionary AffectedUsers, Dictionary AffectedGroups); diff --git a/LightlessAPI/LightlessSyncAPI/Dto/User/CensusDataDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/User/CensusDataDto.cs new file mode 100644 index 0000000..36df050 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/User/CensusDataDto.cs @@ -0,0 +1,6 @@ +using MessagePack; + +namespace LightlessSync.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record CensusDataDto(ushort WorldId, short RaceId, short TribeId, short Gender); \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Dto/User/OnlineUserCharaDataDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/User/OnlineUserCharaDataDto.cs new file mode 100644 index 0000000..122fb78 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/User/OnlineUserCharaDataDto.cs @@ -0,0 +1,7 @@ +using LightlessSync.API.Data; +using MessagePack; + +namespace LightlessSync.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record OnlineUserCharaDataDto(UserData User, CharacterData CharaData) : UserDto(User); \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Dto/User/OnlineUserIdentDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/User/OnlineUserIdentDto.cs new file mode 100644 index 0000000..f8e209b --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/User/OnlineUserIdentDto.cs @@ -0,0 +1,7 @@ +using LightlessSync.API.Data; +using MessagePack; + +namespace LightlessSync.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record OnlineUserIdentDto(UserData User, string Ident) : UserDto(User); \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Dto/User/UserCharaDataMessageDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/User/UserCharaDataMessageDto.cs new file mode 100644 index 0000000..9a28fe2 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/User/UserCharaDataMessageDto.cs @@ -0,0 +1,7 @@ +using LightlessSync.API.Data; +using MessagePack; + +namespace LightlessSync.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record UserCharaDataMessageDto(List Recipients, CharacterData CharaData, CensusDataDto? CensusDataDto); diff --git a/LightlessAPI/LightlessSyncAPI/Dto/User/UserDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/User/UserDto.cs new file mode 100644 index 0000000..1b04b6a --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/User/UserDto.cs @@ -0,0 +1,7 @@ +using LightlessSync.API.Data; +using MessagePack; + +namespace LightlessSync.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record UserDto(UserData User); \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Dto/User/UserIndividualPairStatusDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/User/UserIndividualPairStatusDto.cs new file mode 100644 index 0000000..a418435 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/User/UserIndividualPairStatusDto.cs @@ -0,0 +1,8 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Data.Enum; +using MessagePack; + +namespace LightlessSync.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record UserIndividualPairStatusDto(UserData User, IndividualPairStatus IndividualPairStatus) : UserDto(User); \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Dto/User/UserPairDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/User/UserPairDto.cs new file mode 100644 index 0000000..df1915d --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/User/UserPairDto.cs @@ -0,0 +1,21 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Data.Enum; +using MessagePack; + +namespace LightlessSync.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record UserFullPairDto(UserData User, IndividualPairStatus IndividualPairStatus, List Groups, UserPermissions OwnPermissions, UserPermissions OtherPermissions) : UserDto(User) +{ + public UserPermissions OwnPermissions { get; set; } = OwnPermissions; + public UserPermissions OtherPermissions { get; set; } = OtherPermissions; + public IndividualPairStatus IndividualPairStatus { get; set; } = IndividualPairStatus; +} + +[MessagePackObject(keyAsPropertyName: true)] +public record UserPairDto(UserData User, IndividualPairStatus IndividualPairStatus, UserPermissions OwnPermissions, UserPermissions OtherPermissions) : UserDto(User) +{ + public UserPermissions OwnPermissions { get; set; } = OwnPermissions; + public UserPermissions OtherPermissions { get; set; } = OtherPermissions; + public IndividualPairStatus IndividualPairStatus { get; set; } = IndividualPairStatus; +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Dto/User/UserPermissionsDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/User/UserPermissionsDto.cs new file mode 100644 index 0000000..e8d5dbf --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/User/UserPermissionsDto.cs @@ -0,0 +1,8 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Data.Enum; +using MessagePack; + +namespace LightlessSync.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record UserPermissionsDto(UserData User, UserPermissions Permissions) : UserDto(User); diff --git a/LightlessAPI/LightlessSyncAPI/Dto/User/UserProfileDto.cs b/LightlessAPI/LightlessSyncAPI/Dto/User/UserProfileDto.cs new file mode 100644 index 0000000..bf8a142 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Dto/User/UserProfileDto.cs @@ -0,0 +1,7 @@ +using LightlessSync.API.Data; +using MessagePack; + +namespace LightlessSync.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/LightlessAPI/LightlessSyncAPI/LightlessSync.API.csproj b/LightlessAPI/LightlessSyncAPI/LightlessSync.API.csproj new file mode 100644 index 0000000..8c0e954 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/LightlessSync.API.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/LightlessAPI/LightlessSyncAPI/LightlessSyncAPI.sln b/LightlessAPI/LightlessSyncAPI/LightlessSyncAPI.sln new file mode 100644 index 0000000..21756a4 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/LightlessSyncAPI.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}") = "LightlessSync.API", "LightlessSync.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/LightlessAPI/LightlessSyncAPI/Routes/LightlessAuth.cs b/LightlessAPI/LightlessSyncAPI/Routes/LightlessAuth.cs new file mode 100644 index 0000000..92558d9 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Routes/LightlessAuth.cs @@ -0,0 +1,23 @@ +namespace LightlessSync.API.Routes; + +public class LightlessAuth +{ + public const string OAuth = "/oauth"; + public const string Auth = "/auth"; + public const string Auth_CreateIdent = "createWithIdent"; + public const string Auth_RenewToken = "renewToken"; + public const string OAuth_GetUIDsBasedOnSecretKeys = "getUIDsViaSecretKey"; + public const string OAuth_CreateOAuth = "createWithOAuth"; + public const string OAuth_RenewOAuthToken = "renewToken"; + public const string OAuth_GetDiscordOAuthEndpoint = "getDiscordOAuthEndpoint"; + public const string OAuth_GetUIDs = "getUIDs"; + public const string OAuth_GetDiscordOAuthToken = "getDiscordOAuthToken"; + public static Uri AuthFullPath(Uri baseUri) => new Uri(baseUri, Auth + "/" + Auth_CreateIdent); + public static Uri AuthWithOauthFullPath(Uri baseUri) => new Uri(baseUri, OAuth + "/" + OAuth_CreateOAuth); + public static Uri RenewTokenFullPath(Uri baseUri) => new Uri(baseUri, Auth + "/" + Auth_RenewToken); + public static Uri RenewOAuthTokenFullPath(Uri baseUri) => new Uri(baseUri, OAuth + "/" + OAuth_RenewOAuthToken); + public static Uri GetUIDsBasedOnSecretKeyFullPath(Uri baseUri) => new Uri(baseUri, OAuth + "/" + OAuth_GetUIDsBasedOnSecretKeys); + public static Uri GetDiscordOAuthEndpointFullPath(Uri baseUri) => new Uri(baseUri, OAuth + "/" + OAuth_GetDiscordOAuthEndpoint); + public static Uri GetDiscordOAuthTokenFullPath(Uri baseUri, string sessionId) => new Uri(baseUri, OAuth + "/" + OAuth_GetDiscordOAuthToken + "?sessionId=" + sessionId); + public static Uri GetUIDsFullPath(Uri baseUri) => new Uri(baseUri, OAuth + "/" + OAuth_GetUIDs); +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/Routes/LightlessFiles.cs b/LightlessAPI/LightlessSyncAPI/Routes/LightlessFiles.cs new file mode 100644 index 0000000..ccf6348 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/Routes/LightlessFiles.cs @@ -0,0 +1,47 @@ +namespace LightlessSync.API.Routes; + +public class LightlessFiles +{ + 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_UploadMunged = "uploadMunged"; + public const string ServerFiles_DownloadServers = "downloadServers"; + + public const string Distribution = "/dist"; + public const string Distribution_Get = "get"; + + public const string Main = "/main"; + public const string Main_SendReady = "sendReady"; + + public const string Speedtest = "/speedtest"; + public const string Speedtest_Run = "run"; + + 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 ServerFilesUploadMunged(Uri baseUri, string hash) => new(baseUri, ServerFiles + "/" + ServerFiles_UploadMunged + "/" + hash); + public static Uri ServerFilesGetDownloadServersFullPath(Uri baseUri) => new(baseUri, ServerFiles + "/" + ServerFiles_DownloadServers); + public static Uri DistributionGetFullPath(Uri baseUri, string hash) => new(baseUri, Distribution + "/" + Distribution_Get + "?file=" + hash); + public static Uri SpeedtestRunFullPath(Uri baseUri) => new(baseUri, Speedtest + "/" + Speedtest_Run); + 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/LightlessAPI/LightlessSyncAPI/SignalR/ILightlessHub.cs b/LightlessAPI/LightlessSyncAPI/SignalR/ILightlessHub.cs new file mode 100644 index 0000000..a6c46b2 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/SignalR/ILightlessHub.cs @@ -0,0 +1,91 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Data.Enum; +using LightlessSync.API.Dto; +using LightlessSync.API.Dto.CharaData; +using LightlessSync.API.Dto.Group; +using LightlessSync.API.Dto.User; + +namespace LightlessSync.API.SignalR; + +public interface ILightlessHub +{ + const int ApiVersion = 33; + const string Path = "/lightless"; + + Task CheckClientHealth(); + + Task Client_DownloadReady(Guid requestId); + Task Client_GroupChangePermissions(GroupPermissionDto groupPermission); + Task Client_GroupDelete(GroupDto groupDto); + 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_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_UpdateUserIndividualPairStatusDto(UserIndividualPairStatusDto dto); + Task Client_UserUpdateProfile(UserDto dto); + Task Client_UserUpdateSelfPairPermissions(UserPermissionsDto dto); + Task Client_UserUpdateDefaultPermissions(DefaultPermissionsDto dto); + Task Client_GroupChangeUserPairPermissions(GroupPairUserPermissionDto 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 GroupChangeOwnership(GroupPairDto groupPair); + Task GroupChangePassword(GroupPasswordDto groupPassword); + 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 GroupJoinFinalize(GroupJoinDto passwordedGroup); + Task GroupLeave(GroupDto group); + Task GroupRemoveUser(GroupPairDto groupPair); + Task GroupSetUserInfo(GroupPairUserInfoDto groupPair); + Task> GroupsGetAll(); + Task GroupUnbanUser(GroupPairDto groupPair); + Task GroupPrune(GroupDto group, int days, bool execute); + + Task UserAddPair(UserDto user); + Task UserDelete(); + Task> UserGetOnlinePairs(CensusDataDto? censusDataDto); + Task> UserGetPairedClients(); + Task UserGetProfile(UserDto dto); + Task UserPushData(UserCharaDataMessageDto dto); + Task UserRemovePair(UserDto userDto); + Task UserSetProfile(UserProfileDto userDescription); + Task UserUpdateDefaultPermissions(DefaultPermissionsDto defaultPermissionsDto); + Task SetBulkPermissions(BulkPermissionsDto dto); + + 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); +} \ No newline at end of file diff --git a/LightlessAPI/LightlessSyncAPI/SignalR/ILightlessHubClient.cs b/LightlessAPI/LightlessSyncAPI/SignalR/ILightlessHubClient.cs new file mode 100644 index 0000000..92875f3 --- /dev/null +++ b/LightlessAPI/LightlessSyncAPI/SignalR/ILightlessHubClient.cs @@ -0,0 +1,61 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Data.Enum; +using LightlessSync.API.Dto; +using LightlessSync.API.Dto.CharaData; +using LightlessSync.API.Dto.Group; +using LightlessSync.API.Dto.User; + +namespace LightlessSync.API.SignalR; + +public interface ILightlessHubClient : ILightlessHub +{ + void OnDownloadReady(Action act); + + void OnGroupChangePermissions(Action act); + + void OnGroupDelete(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 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 OnUserDefaultPermissionUpdate(Action act); + + void OnUpdateUserIndividualPairStatusDto(Action act); + + void OnGroupChangeUserPairPermissions(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/LightlessSyncServer/LightlessSyncAuthService/Authentication/SecretKeyAuthReply.cs b/LightlessSyncServer/LightlessSyncAuthService/Authentication/SecretKeyAuthReply.cs new file mode 100644 index 0000000..8130a86 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncAuthService/Authentication/SecretKeyAuthReply.cs @@ -0,0 +1,3 @@ +namespace LightlessSyncAuthService.Authentication; + +public record SecretKeyAuthReply(bool Success, string? Uid, string? PrimaryUid, string? Alias, bool TempBan, bool Permaban, bool MarkedForBan); diff --git a/LightlessSyncServer/LightlessSyncAuthService/Authentication/SecretKeyFailedAuthorization.cs b/LightlessSyncServer/LightlessSyncAuthService/Authentication/SecretKeyFailedAuthorization.cs new file mode 100644 index 0000000..d4c1c06 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncAuthService/Authentication/SecretKeyFailedAuthorization.cs @@ -0,0 +1,12 @@ +namespace LightlessSyncAuthService.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/LightlessSyncServer/LightlessSyncAuthService/Controllers/AuthControllerBase.cs b/LightlessSyncServer/LightlessSyncAuthService/Controllers/AuthControllerBase.cs new file mode 100644 index 0000000..d207db6 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncAuthService/Controllers/AuthControllerBase.cs @@ -0,0 +1,160 @@ +using LightlessSyncAuthService.Authentication; +using LightlessSyncAuthService.Services; +using LightlessSyncShared.Data; +using LightlessSyncShared.Models; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils; +using LightlessSyncShared.Utils.Configuration; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using StackExchange.Redis; +using System.Globalization; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace LightlessSyncAuthService.Controllers; + +public abstract class AuthControllerBase : Controller +{ + protected readonly ILogger Logger; + protected readonly IHttpContextAccessor HttpAccessor; + protected readonly IConfigurationService Configuration; + protected readonly IDbContextFactory LightlessDbContextFactory; + protected readonly SecretKeyAuthenticatorService SecretKeyAuthenticatorService; + private readonly IDatabase _redis; + private readonly GeoIPService _geoIPProvider; + + protected AuthControllerBase(ILogger logger, + IHttpContextAccessor accessor, IDbContextFactory lightlessDbContextFactory, + SecretKeyAuthenticatorService secretKeyAuthenticatorService, + IConfigurationService configuration, + IDatabase redisDb, GeoIPService geoIPProvider) + { + Logger = logger; + HttpAccessor = accessor; + _redis = redisDb; + _geoIPProvider = geoIPProvider; + LightlessDbContextFactory = lightlessDbContextFactory; + SecretKeyAuthenticatorService = secretKeyAuthenticatorService; + Configuration = configuration; + } + + protected async Task GenericAuthResponse(LightlessDbContext dbContext, string charaIdent, SecretKeyAuthReply authResult) + { + if (await IsIdentBanned(dbContext, charaIdent)) + { + Logger.LogWarning("Authenticate:IDENTBAN:{id}:{ident}", authResult.Uid, charaIdent); + return Unauthorized("Your character is banned from using the service."); + } + + if (!authResult.Success && !authResult.TempBan) + { + Logger.LogWarning("Authenticate:INVALID:{id}:{ident}", authResult?.Uid ?? "NOUID", charaIdent); + return Unauthorized("The provided secret key is invalid. Verify your Lightless accounts existence and/or recover the secret key."); + } + if (!authResult.Success && authResult.TempBan) + { + Logger.LogWarning("Authenticate:TEMPBAN:{id}:{ident}", authResult.Uid ?? "NOUID", charaIdent); + return Unauthorized("Due to an excessive amount of failed authentication attempts you are temporarily locked out. Check your Secret Key configuration and try connecting again in 5 minutes."); + } + + if (authResult.Permaban || authResult.MarkedForBan) + { + if (authResult.MarkedForBan) + { + Logger.LogWarning("Authenticate:MARKBAN:{id}:{primaryid}:{ident}", authResult.Uid, authResult.PrimaryUid, charaIdent); + await EnsureBan(authResult.Uid!, authResult.PrimaryUid, charaIdent); + } + + Logger.LogWarning("Authenticate:UIDBAN:{id}:{ident}", authResult.Uid, charaIdent); + return Unauthorized("Your Lightless account is banned from using the service."); + } + + var existingIdent = await _redis.StringGetAsync("UID:" + authResult.Uid); + if (!string.IsNullOrEmpty(existingIdent)) + { + Logger.LogWarning("Authenticate:DUPLICATE:{id}:{ident}", authResult.Uid, charaIdent); + return Unauthorized("Already logged in to this Lightless account. Reconnect in 60 seconds. If you keep seeing this issue, restart your game."); + } + + Logger.LogInformation("Authenticate:SUCCESS:{id}:{ident}", authResult.Uid, charaIdent); + return await CreateJwtFromId(authResult.Uid!, charaIdent, authResult.Alias ?? string.Empty); + } + + protected JwtSecurityToken CreateJwt(IEnumerable authClaims) + { + var authSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration.GetValue(nameof(LightlessConfigurationBase.Jwt)))); + + var token = new SecurityTokenDescriptor() + { + Subject = new ClaimsIdentity(authClaims), + SigningCredentials = new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256Signature), + Expires = new(long.Parse(authClaims.First(f => string.Equals(f.Type, LightlessClaimTypes.Expires, StringComparison.Ordinal)).Value!, CultureInfo.InvariantCulture), DateTimeKind.Utc), + }; + + var handler = new JwtSecurityTokenHandler(); + return handler.CreateJwtSecurityToken(token); + } + + protected async Task CreateJwtFromId(string uid, string charaIdent, string alias) + { + var token = CreateJwt(new List() + { + new Claim(LightlessClaimTypes.Uid, uid), + new Claim(LightlessClaimTypes.CharaIdent, charaIdent), + new Claim(LightlessClaimTypes.Alias, alias), + new Claim(LightlessClaimTypes.Expires, DateTime.UtcNow.AddHours(6).Ticks.ToString(CultureInfo.InvariantCulture)), + new Claim(LightlessClaimTypes.Continent, await _geoIPProvider.GetCountryFromIP(HttpAccessor)) + }); + + return Content(token.RawData); + } + + protected async Task EnsureBan(string uid, string? primaryUid, string charaIdent) + { + using var dbContext = await LightlessDbContextFactory.CreateDbContextAsync(); + if (!dbContext.BannedUsers.Any(c => c.CharacterIdentification == charaIdent)) + { + dbContext.BannedUsers.Add(new Banned() + { + CharacterIdentification = charaIdent, + Reason = "Autobanned CharacterIdent (" + uid + ")", + }); + } + + var uidToLookFor = primaryUid ?? uid; + + var primaryUserAuth = await dbContext.Auth.FirstAsync(f => f.UserUID == uidToLookFor); + primaryUserAuth.MarkForBan = false; + primaryUserAuth.IsBanned = true; + + var lodestone = await dbContext.LodeStoneAuth.Include(a => a.User).FirstOrDefaultAsync(c => c.User.UID == uidToLookFor); + + 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(); + } + + protected async Task IsIdentBanned(LightlessDbContext dbContext, string charaIdent) + { + return await dbContext.BannedUsers.AsNoTracking().AnyAsync(u => u.CharacterIdentification == charaIdent).ConfigureAwait(false); + } +} diff --git a/LightlessSyncServer/LightlessSyncAuthService/Controllers/JwtController.cs b/LightlessSyncServer/LightlessSyncAuthService/Controllers/JwtController.cs new file mode 100644 index 0000000..f89946b --- /dev/null +++ b/LightlessSyncServer/LightlessSyncAuthService/Controllers/JwtController.cs @@ -0,0 +1,89 @@ +using LightlessSync.API.Routes; +using LightlessSyncAuthService.Services; +using LightlessSyncShared; +using LightlessSyncShared.Data; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils; +using LightlessSyncShared.Utils.Configuration; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using StackExchange.Redis; + +namespace LightlessSyncAuthService.Controllers; + +[Route(LightlessAuth.Auth)] +public class JwtController : AuthControllerBase +{ + public JwtController(ILogger logger, + IHttpContextAccessor accessor, IDbContextFactory lightlessDbContextFactory, + SecretKeyAuthenticatorService secretKeyAuthenticatorService, + IConfigurationService configuration, + IDatabase redisDb, GeoIPService geoIPProvider) + : base(logger, accessor, lightlessDbContextFactory, secretKeyAuthenticatorService, + configuration, redisDb, geoIPProvider) + { + } + + [AllowAnonymous] + [HttpPost(LightlessAuth.Auth_CreateIdent)] + public async Task CreateToken(string auth, string charaIdent) + { + using var dbContext = await LightlessDbContextFactory.CreateDbContextAsync(); + return await AuthenticateInternal(dbContext, auth, charaIdent).ConfigureAwait(false); + } + + [Authorize(Policy = "Authenticated")] + [HttpGet(LightlessAuth.Auth_RenewToken)] + public async Task RenewToken() + { + using var dbContext = await LightlessDbContextFactory.CreateDbContextAsync(); + try + { + var uid = HttpContext.User.Claims.Single(p => string.Equals(p.Type, LightlessClaimTypes.Uid, StringComparison.Ordinal))!.Value; + var ident = HttpContext.User.Claims.Single(p => string.Equals(p.Type, LightlessClaimTypes.CharaIdent, StringComparison.Ordinal))!.Value; + var alias = HttpContext.User.Claims.SingleOrDefault(p => string.Equals(p.Type, LightlessClaimTypes.Alias))?.Value ?? string.Empty; + + if (await dbContext.Auth.Where(u => u.UserUID == uid || u.PrimaryUserUID == uid).AnyAsync(a => a.MarkForBan)) + { + var userAuth = await dbContext.Auth.SingleAsync(u => u.UserUID == uid); + await EnsureBan(uid, userAuth.PrimaryUserUID, ident); + + return Unauthorized("Your Lightless account is banned."); + } + + if (await IsIdentBanned(dbContext, ident)) + { + return Unauthorized("Your XIV service account is banned from using the service."); + } + + Logger.LogInformation("RenewToken:SUCCESS:{id}:{ident}", uid, ident); + return await CreateJwtFromId(uid, ident, alias); + } + catch (Exception ex) + { + Logger.LogError(ex, "RenewToken:FAILURE"); + return Unauthorized("Unknown error while renewing authentication token"); + } + } + + protected async Task AuthenticateInternal(LightlessDbContext dbContext, string auth, string charaIdent) + { + try + { + if (string.IsNullOrEmpty(auth)) return BadRequest("No Authkey"); + if (string.IsNullOrEmpty(charaIdent)) return BadRequest("No CharaIdent"); + + var ip = HttpAccessor.GetIpAddress(); + + var authResult = await SecretKeyAuthenticatorService.AuthorizeAsync(ip, auth); + + return await GenericAuthResponse(dbContext, charaIdent, authResult); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Authenticate:UNKNOWN"); + return Unauthorized("Unknown internal server error during authentication"); + } + } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncAuthService/Controllers/OAuthController.cs b/LightlessSyncServer/LightlessSyncAuthService/Controllers/OAuthController.cs new file mode 100644 index 0000000..e3c4916 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncAuthService/Controllers/OAuthController.cs @@ -0,0 +1,307 @@ +using LightlessSync.API.Routes; +using LightlessSyncAuthService.Services; +using LightlessSyncShared; +using LightlessSyncShared.Data; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils; +using LightlessSyncShared.Utils.Configuration; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using StackExchange.Redis; +using System.Collections.Concurrent; +using System.Globalization; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text.Json; +using System.Web; + +namespace LightlessSyncAuthService.Controllers; + +[Route(LightlessAuth.OAuth)] +public class OAuthController : AuthControllerBase +{ + private const string _discordOAuthCall = "discordCall"; + private const string _discordOAuthCallback = "discordCallback"; + private static readonly ConcurrentDictionary _cookieOAuthResponse = []; + + public OAuthController(ILogger logger, + IHttpContextAccessor accessor, IDbContextFactory lightlessDbContext, + SecretKeyAuthenticatorService secretKeyAuthenticatorService, + IConfigurationService configuration, + IDatabase redisDb, GeoIPService geoIPProvider) + : base(logger, accessor, lightlessDbContext, secretKeyAuthenticatorService, + configuration, redisDb, geoIPProvider) + { + } + + [AllowAnonymous] + [HttpGet(_discordOAuthCall)] + public IActionResult DiscordOAuthSetCookieAndRedirect([FromQuery] string sessionId) + { + var discordOAuthUri = Configuration.GetValueOrDefault(nameof(AuthServiceConfiguration.PublicOAuthBaseUri), null); + var discordClientSecret = Configuration.GetValueOrDefault(nameof(AuthServiceConfiguration.DiscordOAuthClientSecret), null); + var discordClientId = Configuration.GetValueOrDefault(nameof(AuthServiceConfiguration.DiscordOAuthClientId), null); + if (discordClientSecret == null || discordClientId == null || discordOAuthUri == null) + return BadRequest("Server does not support OAuth2"); + + Logger.LogDebug("Starting OAuth Process for {session}", sessionId); + + var cookieOptions = new CookieOptions + { + HttpOnly = true, + Secure = true, + Expires = DateTime.UtcNow.AddMinutes(30) + }; + Response.Cookies.Append("DiscordOAuthSessionCookie", sessionId, cookieOptions); + + var parameters = new Dictionary + { + { "client_id", discordClientId }, + { "response_type", "code" }, + { "redirect_uri", new Uri(discordOAuthUri, _discordOAuthCallback).ToString() }, + { "scope", "identify"}, + }; + using var content = new FormUrlEncodedContent(parameters); + UriBuilder builder = new UriBuilder("https://discord.com/oauth2/authorize"); + var query = HttpUtility.ParseQueryString(builder.Query); + foreach (var param in parameters) + { + query[param.Key] = param.Value; + } + builder.Query = query.ToString(); + + return Redirect(builder.ToString()); + } + + [AllowAnonymous] + [HttpGet(_discordOAuthCallback)] + public async Task DiscordOAuthCallback([FromQuery] string code) + { + var reqId = Request.Cookies["DiscordOAuthSessionCookie"]; + + var discordOAuthUri = Configuration.GetValueOrDefault(nameof(AuthServiceConfiguration.PublicOAuthBaseUri), null); + var discordClientSecret = Configuration.GetValueOrDefault(nameof(AuthServiceConfiguration.DiscordOAuthClientSecret), null); + var discordClientId = Configuration.GetValueOrDefault(nameof(AuthServiceConfiguration.DiscordOAuthClientId), null); + if (discordClientSecret == null || discordClientId == null || discordOAuthUri == null) + return BadRequest("Server does not support OAuth2"); + if (string.IsNullOrEmpty(reqId)) return BadRequest("No session cookie found"); + if (string.IsNullOrEmpty(code)) return BadRequest("No Discord OAuth2 code found"); + + Logger.LogDebug("Discord OAuth Callback for {session}", reqId); + + var query = HttpUtility.ParseQueryString(discordOAuthUri.Query); + using var client = new HttpClient(); + var parameters = new Dictionary + { + { "client_id", discordClientId }, + { "client_secret", discordClientSecret }, + { "grant_type", "authorization_code" }, + { "code", code }, + { "redirect_uri", new Uri(discordOAuthUri, _discordOAuthCallback).ToString() } + }; + + using var content = new FormUrlEncodedContent(parameters); + using var response = await client.PostAsync("https://discord.com/api/oauth2/token", content); + using var responseBody = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + + if (!response.IsSuccessStatusCode) + { + Logger.LogDebug("Failed to get Discord token for {session}", reqId); + return BadRequest("Failed to get Discord token"); + } + + using var tokenJson = await JsonDocument.ParseAsync(responseBody).ConfigureAwait(false); + var token = tokenJson.RootElement.GetProperty("access_token").GetString(); + + using var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + using var meResponse = await httpClient.GetAsync("https://discord.com/api/users/@me"); + using var meBody = await meResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); + + if (!meResponse.IsSuccessStatusCode) + { + Logger.LogDebug("Failed to get Discord user info for {session}", reqId); + return BadRequest("Failed to get Discord user info"); + } + + ulong discordUserId = 0; + string discordUserName = string.Empty; + try + { + using var jsonResponse = await JsonDocument.ParseAsync(meBody).ConfigureAwait(false); + discordUserId = ulong.Parse(jsonResponse.RootElement.GetProperty("id").GetString()!); + discordUserName = jsonResponse.RootElement.GetProperty("username").GetString()!; + } + catch (Exception ex) + { + Logger.LogDebug(ex, "Failed to parse Discord user info for {session}", reqId); + return BadRequest("Failed to parse user id from @me response for token"); + } + + if (discordUserId == 0) + return BadRequest("Failed to get Discord ID from login token"); + + using var dbContext = await LightlessDbContextFactory.CreateDbContextAsync(); + + var lightlessUser = await dbContext.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.DiscordId == discordUserId); + if (lightlessUser == default) + { + Logger.LogDebug("Failed to get Lightless user for {session}, DiscordId: {id}", reqId, discordUserId); + + return BadRequest("Could not find a Lightless user associated to this Discord account."); + } + + JwtSecurityToken? jwt = null; + try + { + jwt = CreateJwt([ + new Claim(LightlessClaimTypes.Uid, lightlessUser.User!.UID), + new Claim(LightlessClaimTypes.Expires, DateTime.UtcNow.AddDays(14).Ticks.ToString(CultureInfo.InvariantCulture)), + new Claim(LightlessClaimTypes.DiscordId, discordUserId.ToString()), + new Claim(LightlessClaimTypes.DiscordUser, discordUserName), + new Claim(LightlessClaimTypes.OAuthLoginToken, true.ToString()) + ]); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to create the OAuth2 token for session {session} and Discord user {user}", reqId, discordUserId); + return BadRequest("Failed to create the OAuth2 token. Please contact the developer for more information."); + } + + _cookieOAuthResponse[reqId] = jwt.RawData; + _ = Task.Run(async () => + { + bool isRemoved = false; + for (int i = 0; i < 30; i++) + { + await Task.Delay(TimeSpan.FromSeconds(3)).ConfigureAwait(false); + if (!_cookieOAuthResponse.ContainsKey(reqId)) + { + isRemoved = true; + break; + } + } + if (!isRemoved) + _cookieOAuthResponse.TryRemove(reqId, out _); + }); + + Logger.LogDebug("Setting JWT response for {session}, process complete", reqId); + return Ok("The OAuth2 token was generated. The plugin will grab it automatically. You can close this browser tab."); + } + + [Authorize(Policy = "OAuthToken")] + [HttpPost(LightlessAuth.OAuth_GetUIDsBasedOnSecretKeys)] + public async Task> GetUIDsBasedOnSecretKeys([FromBody] List secretKeys) + { + if (!secretKeys.Any()) + return []; + + using var dbContext = await LightlessDbContextFactory.CreateDbContextAsync(); + + Dictionary secretKeysToUIDDict = secretKeys.Distinct().ToDictionary(k => k, _ => string.Empty, StringComparer.Ordinal); + foreach (var key in secretKeys) + { + var shaKey = StringUtils.Sha256String(key); + var associatedAuth = await dbContext.Auth.AsNoTracking().SingleOrDefaultAsync(a => a.HashedKey == shaKey); + if (associatedAuth != null) + { + secretKeysToUIDDict[key] = associatedAuth.UserUID; + } + } + + return secretKeysToUIDDict; + } + + [Authorize(Policy = "OAuthToken")] + [HttpPost(LightlessAuth.OAuth_RenewOAuthToken)] + public IActionResult RenewOAuthToken() + { + var claims = HttpContext.User.Claims.Where(c => c.Type != LightlessClaimTypes.Expires).ToList(); + claims.Add(new Claim(LightlessClaimTypes.Expires, DateTime.UtcNow.AddDays(14).Ticks.ToString(CultureInfo.InvariantCulture))); + return Content(CreateJwt(claims).RawData); + } + + [AllowAnonymous] + [HttpGet(LightlessAuth.OAuth_GetDiscordOAuthToken)] + public async Task GetDiscordOAuthToken([FromQuery] string sessionId) + { + Logger.LogDebug("Starting to wait for GetDiscordOAuthToken for {session}", sessionId); + using CancellationTokenSource cts = new(); + cts.CancelAfter(TimeSpan.FromSeconds(55)); + while (!_cookieOAuthResponse.ContainsKey(sessionId) && !cts.Token.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromSeconds(1), cts.Token); + } + if (cts.IsCancellationRequested) + { + Logger.LogDebug("Timeout elapsed for {session}", sessionId); + return BadRequest("Did not find Discord OAuth2 response"); + } + _cookieOAuthResponse.TryRemove(sessionId, out var token); + if (token == null) + { + Logger.LogDebug("No token found for {session}", sessionId); + return BadRequest("OAuth session was never established"); + } + Logger.LogDebug("Returning JWT for {session}, process complete", sessionId); + return Content(token); + } + + [AllowAnonymous] + [HttpGet(LightlessAuth.OAuth_GetDiscordOAuthEndpoint)] + public Uri? GetDiscordOAuthEndpoint() + { + var discordOAuthUri = Configuration.GetValueOrDefault(nameof(AuthServiceConfiguration.PublicOAuthBaseUri), null); + var discordClientSecret = Configuration.GetValueOrDefault(nameof(AuthServiceConfiguration.DiscordOAuthClientSecret), null); + var discordClientId = Configuration.GetValueOrDefault(nameof(AuthServiceConfiguration.DiscordOAuthClientId), null); + if (discordClientSecret == null || discordClientId == null || discordOAuthUri == null) + return null; + return new Uri(discordOAuthUri, _discordOAuthCall); + } + + [Authorize(Policy = "OAuthToken")] + [HttpGet(LightlessAuth.OAuth_GetUIDs)] + public async Task> GetAvailableUIDs() + { + string primaryUid = HttpContext.User.Claims.Single(c => string.Equals(c.Type, LightlessClaimTypes.Uid, StringComparison.Ordinal))!.Value; + using var dbContext = await LightlessDbContextFactory.CreateDbContextAsync(); + + var lightlessUser = await dbContext.Auth.AsNoTracking().Include(u => u.User).FirstOrDefaultAsync(f => f.UserUID == primaryUid).ConfigureAwait(false); + if (lightlessUser == null || lightlessUser.User == null) return []; + var uid = lightlessUser.User.UID; + var allUids = await dbContext.Auth.AsNoTracking().Include(u => u.User).Where(a => a.UserUID == uid || a.PrimaryUserUID == uid).ToListAsync().ConfigureAwait(false); + var result = allUids.OrderBy(u => u.UserUID == uid ? 0 : 1).ThenBy(u => u.UserUID).Select(u => (u.UserUID, u.User.Alias)).ToDictionary(); + return result; + } + + [Authorize(Policy = "OAuthToken")] + [HttpPost(LightlessAuth.OAuth_CreateOAuth)] + public async Task CreateTokenWithOAuth(string uid, string charaIdent) + { + using var dbContext = await LightlessDbContextFactory.CreateDbContextAsync(); + + return await AuthenticateOAuthInternal(dbContext, uid, charaIdent); + } + + private async Task AuthenticateOAuthInternal(LightlessDbContext dbContext, string requestedUid, string charaIdent) + { + try + { + string primaryUid = HttpContext.User.Claims.Single(c => string.Equals(c.Type, LightlessClaimTypes.Uid, StringComparison.Ordinal))!.Value; + if (string.IsNullOrEmpty(requestedUid)) return BadRequest("No UID"); + if (string.IsNullOrEmpty(charaIdent)) return BadRequest("No CharaIdent"); + + var ip = HttpAccessor.GetIpAddress(); + + var authResult = await SecretKeyAuthenticatorService.AuthorizeOauthAsync(ip, primaryUid, requestedUid); + + return await GenericAuthResponse(dbContext, charaIdent, authResult); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Authenticate:UNKNOWN"); + return Unauthorized("Unknown internal server error during authentication"); + } + } +} diff --git a/LightlessSyncServer/LightlessSyncAuthService/LightlessSyncAuthService.csproj b/LightlessSyncServer/LightlessSyncAuthService/LightlessSyncAuthService.csproj new file mode 100644 index 0000000..76899a7 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncAuthService/LightlessSyncAuthService.csproj @@ -0,0 +1,22 @@ + + + + net9.0 + enable + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/LightlessSyncServer/LightlessSyncAuthService/Program.cs b/LightlessSyncServer/LightlessSyncAuthService/Program.cs new file mode 100644 index 0000000..582dd8d --- /dev/null +++ b/LightlessSyncServer/LightlessSyncAuthService/Program.cs @@ -0,0 +1,55 @@ +namespace LightlessSyncAuthService; + +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() + .UseConsoleLifetime() + .ConfigureAppConfiguration((ctx, config) => + { + var appSettingsPath = Environment.GetEnvironmentVariable("APPSETTINGS_PATH"); + if (!string.IsNullOrEmpty(appSettingsPath)) + { + config.AddJsonFile(appSettingsPath, optional: true, reloadOnChange: true); + } + else + { + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); + } + + config.AddEnvironmentVariables(); + }) + .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/LightlessSyncServer/LightlessSyncAuthService/Properties/launchSettings.json b/LightlessSyncServer/LightlessSyncAuthService/Properties/launchSettings.json new file mode 100644 index 0000000..db41dc1 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncAuthService/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/LightlessSyncServer/LightlessSyncAuthService/Services/GeoIPService.cs b/LightlessSyncServer/LightlessSyncAuthService/Services/GeoIPService.cs new file mode 100644 index 0000000..2ea048f --- /dev/null +++ b/LightlessSyncServer/LightlessSyncAuthService/Services/GeoIPService.cs @@ -0,0 +1,143 @@ +using LightlessSyncShared; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils.Configuration; +using MaxMind.GeoIP2; + +namespace LightlessSyncAuthService.Services; + +public class GeoIPService : IHostedService +{ + private readonly ILogger _logger; + private readonly IConfigurationService _lightlessConfiguration; + private bool _useGeoIP = false; + private string _cityFile = string.Empty; + private DatabaseReader? _dbReader; + private DateTime _dbLastWriteTime = DateTime.MinValue; + private CancellationTokenSource _fileWriteTimeCheckCts = new(); + private bool _processingReload = false; + + public GeoIPService(ILogger logger, + IConfigurationService lightlessConfiguration) + { + _logger = logger; + _lightlessConfiguration = lightlessConfiguration; + } + + 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 = _lightlessConfiguration.GetValueOrDefault(nameof(AuthServiceConfiguration.UseGeoIP), false); + var cityFile = _lightlessConfiguration.GetValueOrDefault(nameof(AuthServiceConfiguration.GeoIPDbCityFile), string.Empty); + DateTime lastWriteTime = DateTime.MinValue; + if (File.Exists(cityFile)) + { + 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/LightlessSyncServer/LightlessSyncAuthService/Services/SecretKeyAuthenticatorService.cs b/LightlessSyncServer/LightlessSyncAuthService/Services/SecretKeyAuthenticatorService.cs new file mode 100644 index 0000000..aa35029 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncAuthService/Services/SecretKeyAuthenticatorService.cs @@ -0,0 +1,132 @@ +using System.Collections.Concurrent; +using LightlessSyncAuthService.Authentication; +using LightlessSyncShared.Data; +using LightlessSyncShared.Metrics; +using LightlessSyncShared.Models; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils.Configuration; +using Microsoft.EntityFrameworkCore; + +namespace LightlessSyncAuthService.Services; + +public class SecretKeyAuthenticatorService +{ + private readonly LightlessMetrics _metrics; + private readonly IDbContextFactory _dbContextFactory; + private readonly IConfigurationService _configurationService; + private readonly ILogger _logger; + private readonly ConcurrentDictionary _failedAuthorizations = new(StringComparer.Ordinal); + + public SecretKeyAuthenticatorService(LightlessMetrics metrics, IDbContextFactory dbContextFactory, + IConfigurationService configuration, ILogger logger) + { + _logger = logger; + _configurationService = configuration; + _metrics = metrics; + _dbContextFactory = dbContextFactory; + } + + public async Task AuthorizeOauthAsync(string ip, string primaryUid, string requestedUid) + { + _metrics.IncCounter(MetricsAPI.CounterAuthenticationRequests); + + var checkOnIp = FailOnIp(ip); + if (checkOnIp != null) return checkOnIp; + + using var context = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false); + var authUser = await context.Auth.SingleOrDefaultAsync(u => u.UserUID == primaryUid).ConfigureAwait(false); + if (authUser == null) return AuthenticationFailure(ip); + + var authReply = await context.Auth.Include(a => a.User).AsNoTracking() + .SingleOrDefaultAsync(u => u.UserUID == requestedUid).ConfigureAwait(false); + return await GetAuthReply(ip, context, authReply); + } + + public async Task AuthorizeAsync(string ip, string hashedSecretKey) + { + _metrics.IncCounter(MetricsAPI.CounterAuthenticationRequests); + + var checkOnIp = FailOnIp(ip); + if (checkOnIp != null) return checkOnIp; + + using var context = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false); + var authReply = await context.Auth.Include(a => a.User).AsNoTracking() + .SingleOrDefaultAsync(u => u.HashedKey == hashedSecretKey).ConfigureAwait(false); + return await GetAuthReply(ip, context, authReply).ConfigureAwait(false); + } + + private async Task GetAuthReply(string ip, LightlessDbContext context, Auth? authReply) + { + var isBanned = authReply?.IsBanned ?? false; + var markedForBan = authReply?.MarkForBan ?? false; + var primaryUid = authReply?.PrimaryUserUID ?? authReply?.UserUID; + + if (authReply?.PrimaryUserUID != null) + { + var primaryUser = await context.Auth.AsNoTracking().SingleAsync(u => u.UserUID == authReply.PrimaryUserUID).ConfigureAwait(false); + isBanned = isBanned || primaryUser.IsBanned; + markedForBan = markedForBan || primaryUser.MarkForBan; + } + + SecretKeyAuthReply reply = new(authReply != null, authReply?.UserUID, + authReply?.PrimaryUserUID ?? authReply?.UserUID, authReply?.User?.Alias ?? string.Empty, + TempBan: false, isBanned, markedForBan); + + if (reply.Success) + { + _metrics.IncCounter(MetricsAPI.CounterAuthenticationSuccesses); + _metrics.IncGauge(MetricsAPI.GaugeAuthenticationCacheEntries); + return reply; + } + else + { + return AuthenticationFailure(ip); + } + } + + private SecretKeyAuthReply? FailOnIp(string ip) + { + 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, PrimaryUid: null, Alias: null, TempBan: true, Permaban: false, MarkedForBan: false); + } + + return null; + } + + 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.Exists(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, PrimaryUid: null, Alias: null, TempBan: false, Permaban: false, MarkedForBan: false); + } +} diff --git a/LightlessSyncServer/LightlessSyncAuthService/Startup.cs b/LightlessSyncServer/LightlessSyncAuthService/Startup.cs new file mode 100644 index 0000000..385fa32 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncAuthService/Startup.cs @@ -0,0 +1,244 @@ +using LightlessSyncAuthService.Controllers; +using LightlessSyncShared.Metrics; +using LightlessSyncShared.Services; +using LightlessSyncShared.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 LightlessSyncAuthService.Services; +using LightlessSyncShared.RequirementHandlers; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using System.Text; +using LightlessSyncShared.Data; +using Microsoft.EntityFrameworkCore; +using Prometheus; +using LightlessSyncShared.Utils.Configuration; +using StackExchange.Redis.Extensions.Core.Abstractions; + +namespace LightlessSyncAuthService; + +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(LightlessConfigurationBase.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 lightlessConfig = _configuration.GetRequiredSection("LightlessSync"); + + services.AddHttpContextAccessor(); + + ConfigureRedis(services, lightlessConfig); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddHostedService(provider => provider.GetRequiredService()); + + services.Configure(_configuration.GetRequiredSection("LightlessSync")); + services.Configure(_configuration.GetRequiredSection("LightlessSync")); + + services.AddSingleton(); + + ConfigureAuthorization(services); + + ConfigureDatabase(services, lightlessConfig); + + ConfigureConfigServices(services); + + ConfigureMetrics(services); + + services.AddHealthChecks(); + services.AddControllers().ConfigureApplicationPartManager(a => + { + a.FeatureProviders.Remove(a.FeatureProviders.OfType().First()); + a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(JwtController), typeof(OAuthController))); + }); + } + + private static void ConfigureAuthorization(IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + 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(LightlessConfigurationBase.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("OAuthToken", policy => + { + policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme); + policy.AddRequirements(new ValidTokenRequirement()); + policy.AddRequirements(new ExistingUserRequirement()); + policy.RequireClaim(LightlessClaimTypes.OAuthLoginToken, "True"); + }); + options.AddPolicy("Authenticated", policy => + { + policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme); + policy.RequireAuthenticatedUser(); + policy.AddRequirements(new ValidTokenRequirement()); + }); + options.AddPolicy("Identified", policy => + { + policy.AddRequirements(new UserRequirement(UserRequirements.Identified)); + policy.AddRequirements(new ValidTokenRequirement()); + + }); + options.AddPolicy("Admin", policy => + { + policy.AddRequirements(new UserRequirement(UserRequirements.Identified | UserRequirements.Administrator)); + policy.AddRequirements(new ValidTokenRequirement()); + + }); + options.AddPolicy("Moderator", policy => + { + policy.AddRequirements(new UserRequirement(UserRequirements.Identified | UserRequirements.Moderator | UserRequirements.Administrator)); + policy.AddRequirements(new ValidTokenRequirement()); + }); + options.AddPolicy("Internal", new AuthorizationPolicyBuilder().RequireClaim(LightlessClaimTypes.Internal, "true").Build()); + }); + } + + private static void ConfigureMetrics(IServiceCollection services) + { + services.AddSingleton(m => new LightlessMetrics(m.GetService>(), new List + { + MetricsAPI.CounterAuthenticationCacheHits, + MetricsAPI.CounterAuthenticationFailures, + MetricsAPI.CounterAuthenticationRequests, + MetricsAPI.CounterAuthenticationSuccesses, + }, new List + { + MetricsAPI.GaugeAuthenticationCacheEntries, + })); + } + + private void ConfigureRedis(IServiceCollection services, IConfigurationSection lightlessConfig) + { + // configure redis for SignalR + var redisConnection = lightlessConfig.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 = lightlessConfig.GetValue(nameof(ServerConfiguration.RedisPool), 50), + SyncTimeout = options.SyncTimeout, + };*/ + + var muxer = ConnectionMultiplexer.Connect(options); + var db = muxer.GetDatabase(); + services.AddSingleton(db); + + _logger.LogInformation("Setting up Redis to connect to {host}:{port}", address, port); + } + private void ConfigureConfigServices(IServiceCollection services) + { + services.AddSingleton, LightlessConfigurationServiceServer>(); + services.AddSingleton, LightlessConfigurationServiceServer>(); + } + + private void ConfigureDatabase(IServiceCollection services, IConfigurationSection lightlessConfig) + { + services.AddDbContextPool(options => + { + options.UseNpgsql(_configuration.GetConnectionString("DefaultConnection"), builder => + { + builder.MigrationsHistoryTable("_efmigrationshistory", "public"); + builder.MigrationsAssembly("LightlessSyncShared"); + }).UseSnakeCaseNamingConvention(); + options.EnableThreadSafetyChecks(false); + }, lightlessConfig.GetValue(nameof(LightlessConfigurationBase.DbContextPoolSize), 1024)); + services.AddDbContextFactory(options => + { + options.UseNpgsql(_configuration.GetConnectionString("DefaultConnection"), builder => + { + builder.MigrationsHistoryTable("_efmigrationshistory", "public"); + builder.MigrationsAssembly("LightlessSyncShared"); + }).UseSnakeCaseNamingConvention(); + options.EnableThreadSafetyChecks(false); + }); + } +} diff --git a/LightlessSyncServer/LightlessSyncAuthService/appsettings.Development.json b/LightlessSyncServer/LightlessSyncAuthService/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/LightlessSyncServer/LightlessSyncAuthService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/LightlessSyncServer/LightlessSyncAuthService/appsettings.json b/LightlessSyncServer/LightlessSyncAuthService/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncAuthService/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/LightlessSyncServer/LightlessSyncServer.sln b/LightlessSyncServer/LightlessSyncServer.sln new file mode 100644 index 0000000..f194ab8 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer.sln @@ -0,0 +1,65 @@ + +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}") = "LightlessSyncServer", "LightlessSyncServer\LightlessSyncServer.csproj", "{029CA97F-E0BA-4172-A191-EA21FB61AD0F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LightlessSyncServerTest", "LightlessSyncServerTest\LightlessSyncServerTest.csproj", "{25A82A2A-35C2-4EE0-A0E8-DFDD77978DDA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LightlessSyncShared", "LightlessSyncShared\LightlessSyncShared.csproj", "{67B1461D-E215-4BA8-A64D-E1836724D5E6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LightlessSyncStaticFilesServer", "LightlessSyncStaticFilesServer\LightlessSyncStaticFilesServer.csproj", "{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LightlessSyncServices", "LightlessSyncServices\LightlessSyncServices.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}") = "LightlessSyncAuthService", "LightlessSyncAuthService\LightlessSyncAuthService.csproj", "{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightlessSync.API", "..\LightlessAPI\LightlessSyncAPI\LightlessSync.API.csproj", "{9A0E2DF9-5E1B-9807-2957-63765888660E}" +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 + {25A82A2A-35C2-4EE0-A0E8-DFDD77978DDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25A82A2A-35C2-4EE0-A0E8-DFDD77978DDA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25A82A2A-35C2-4EE0-A0E8-DFDD77978DDA}.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 + {9A0E2DF9-5E1B-9807-2957-63765888660E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A0E2DF9-5E1B-9807-2957-63765888660E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A0E2DF9-5E1B-9807-2957-63765888660E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A0E2DF9-5E1B-9807-2957-63765888660E}.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/LightlessSyncServer/LightlessSyncServer/Controllers/ClientMessageController.cs b/LightlessSyncServer/LightlessSyncServer/Controllers/ClientMessageController.cs new file mode 100644 index 0000000..0a6d5e8 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Controllers/ClientMessageController.cs @@ -0,0 +1,42 @@ +using LightlessSync.API.SignalR; +using LightlessSyncServer.Hubs; +using LightlessSyncShared.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; + +namespace LightlessSyncServer.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(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/LightlessSyncServer/LightlessSyncServer/Hubs/ConcurrencyFilter.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/ConcurrencyFilter.cs new file mode 100644 index 0000000..746a5e0 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/ConcurrencyFilter.cs @@ -0,0 +1,108 @@ +using LightlessSyncShared.Metrics; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils.Configuration; +using Microsoft.AspNetCore.SignalR; +using System.Threading.RateLimiting; + +namespace LightlessSyncServer.Hubs; + +public sealed class ConcurrencyFilter : IHubFilter, IDisposable +{ + private ConcurrencyLimiter _limiter; + private int _setLimit = 0; + private readonly IConfigurationService _config; + private readonly CancellationTokenSource _cts = new(); + + private bool _disposed; + + public ConcurrencyFilter(IConfigurationService config, LightlessMetrics lightlessMetrics) + { + _config = config; + _config.ConfigChangedEvent += OnConfigChange; + + RecreateLimiter(); + + _ = Task.Run(async () => + { + var token = _cts.Token; + while (!token.IsCancellationRequested) + { + var stats = _limiter?.GetStatistics(); + if (stats != null) + { + lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeHubConcurrency, stats.CurrentAvailablePermits); + lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeHubQueuedConcurrency, stats.CurrentQueuedCount); + } + await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); + } + }); + } + + private void OnConfigChange(object sender, EventArgs e) + { + RecreateLimiter(); + } + + private void RecreateLimiter() + { + var newLimit = _config.GetValueOrDefault(nameof(ServerConfiguration.HubExecutionConcurrencyFilter), 50); + + if (newLimit == _setLimit && _limiter is not null) + { + return; + } + + _setLimit = newLimit; + _limiter?.Dispose(); + _limiter = new(new ConcurrencyLimiterOptions() + { + PermitLimit = newLimit, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = newLimit * 100, + }); + } + + public async ValueTask InvokeMethodAsync( + HubInvocationContext invocationContext, Func> next) + { + if (string.Equals(invocationContext.HubMethodName, nameof(LightlessHub.CheckClientHealth), StringComparison.Ordinal)) + { + return await next(invocationContext).ConfigureAwait(false); + } + + var ct = invocationContext.Context.ConnectionAborted; + RateLimitLease lease; + try + { + lease = await _limiter.AcquireAsync(1, ct).ConfigureAwait(false); + } + catch (OperationCanceledException) when (ct.IsCancellationRequested) + { + throw; + } + + if (!lease.IsAcquired) + { + throw new HubException("Concurrency limit exceeded. Try again later."); + } + + using (lease) + { + return await next(invocationContext).ConfigureAwait(false); + } + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _cts.Cancel(); + _limiter?.Dispose(); + _config.ConfigChangedEvent -= OnConfigChange; + _cts.Dispose(); + } +} diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.CharaData.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.CharaData.cs new file mode 100644 index 0000000..ad121d8 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.CharaData.cs @@ -0,0 +1,641 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Dto.CharaData; +using LightlessSyncServer.Utils; +using LightlessSyncShared.Models; +using LightlessSyncShared.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.EntityFrameworkCore; +using System.Text.Json; + +namespace LightlessSyncServer.Hubs; + +public partial class LightlessHub +{ + [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 = string.IsNullOrEmpty(user.Alias) ? _maxCharaDataByUser : _maxCharaDataByUserVanity; + 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(LightlessHubLogger.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(LightlessHubLogger.Args("SUCCESS", id)); + + DbContext.Remove(existingData); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + return true; + } + catch (Exception ex) + { + _logger.LogCallWarning(LightlessHubLogger.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(LightlessHubLogger.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(LightlessHubLogger.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(LightlessHubLogger.Args("SUCCESS")); + return [.. ownCharaData.Select(GetCharaDataFullDto)]; + } + + [Authorize(Policy = "Identified")] + public async Task CharaDataAttemptRestore(string id) + { + _logger.LogCallInfo(LightlessHubLogger.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 pairs = (await GetAllPairInfo(UserUID).ConfigureAwait(false)); + var individualPairs = pairs.Where(p => p.Value.IndividuallyPaired && (!p.Value.OwnPermissions?.IsPaused ?? false) && (!p.Value.OtherPermissions?.IsPaused ?? false)).Select(k => k.Key).ToList(); + var allPairs = pairs.Where(p => (!p.Value.OwnPermissions?.IsPaused ?? false) && (!p.Value.OtherPermissions?.IsPaused ?? false)).Select(k => k.Key).ToList(); + + 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.AccessType == CharaDataAccess.Public)) + || (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(LightlessHubLogger.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(LightlessHubLogger.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; + + if (charaData.AllowedIndividiuals.Any(u => joinedGroups.Contains(u.AllowedGroupGID, StringComparer.Ordinal))) + return true; + + var pairInfoUploader = await GetAllPairInfo(charaData.UploaderUID).ConfigureAwait(false); + + // check for all pairs + if (charaData.AccessType == CharaDataAccess.AllPairs) + { + if (pairInfoUploader.TryGetValue(UserUID, out var userInfo) && userInfo.IsSynced && !userInfo.OwnPermissions.IsPaused && !userInfo.OtherPermissions.IsPaused) + { + return true; + } + + return false; + } + + // check for individual pairs + if (charaData.AccessType == CharaDataAccess.ClosePairs) + { + if (pairInfoUploader.TryGetValue(UserUID, out var userInfo) && userInfo.IsSynced && !userInfo.OwnPermissions.IsPaused && !userInfo.OtherPermissions.IsPaused + && userInfo.IndividuallyPaired) + { + return true; + } + + return false; + } + + return false; + } + + private async Task GetCharaDataById(string id, string methodName) + { + var splitid = id.Split(":", StringSplitOptions.None); + if (splitid.Length != 2) + { + _logger.LogCallWarning(LightlessHubLogger.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(LightlessHubLogger.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(LightlessHubLogger.Args("UNAUTHORIZED", id)); + throw new UnauthorizedAccessException($"User is not allowed to download {id}"); + } + + return charaData; + } +} diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.ClientStubs.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.ClientStubs.cs new file mode 100644 index 0000000..d148f91 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.ClientStubs.cs @@ -0,0 +1,58 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Data.Enum; +using LightlessSync.API.Dto; +using LightlessSync.API.Dto.CharaData; +using LightlessSync.API.Dto.Group; +using LightlessSync.API.Dto.User; + +namespace LightlessSyncServer.Hubs +{ + public partial class LightlessHub + { + 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_GroupDelete(GroupDto groupDto) => 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_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_UserUpdateDefaultPermissions(DefaultPermissionsDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + public Task Client_UpdateUserIndividualPairStatusDto(UserIndividualPairStatusDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + public Task Client_GroupChangeUserPairPermissions(GroupPairUserPermissionDto 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/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Functions.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Functions.cs new file mode 100644 index 0000000..fafc432 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Functions.cs @@ -0,0 +1,488 @@ +using LightlessSyncShared.Models; +using Microsoft.EntityFrameworkCore; +using LightlessSyncServer.Utils; +using LightlessSyncShared.Utils; +using LightlessSync.API.Data; +using LightlessSync.API.Dto.Group; +using LightlessSyncShared.Metrics; +using Microsoft.AspNetCore.SignalR; + +namespace LightlessSyncServer.Hubs; + +public partial class LightlessHub +{ + public string UserCharaIdent => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, LightlessClaimTypes.CharaIdent, StringComparison.Ordinal))?.Value ?? throw new Exception("No Chara Ident in Claims"); + + public string UserUID => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, LightlessClaimTypes.Uid, StringComparison.Ordinal))?.Value ?? throw new Exception("No UID in Claims"); + + public string Continent => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, LightlessClaimTypes.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 defaultpermissions = await DbContext.UserDefaultPreferredPermissions.SingleOrDefaultAsync(u => u.UserUID == user.UID).ConfigureAwait(false); + var groupPermissions = await DbContext.GroupPairPreferredPermissions.Where(u => u.UserUID == user.UID).ToListAsync().ConfigureAwait(false); + var individualPermissions = await DbContext.Permissions.Where(u => u.UserUID == user.UID || u.OtherUserUID == user.UID).ToListAsync().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); + } + + while (DbContext.Files.Any(f => f.Uploader == user)) + { + await Task.Delay(1000).ConfigureAwait(false); + } + + DbContext.ClientPairs.RemoveRange(ownPairData); + 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); + } + + if (defaultpermissions != null) + { + DbContext.UserDefaultPreferredPermissions.Remove(defaultpermissions); + } + DbContext.GroupPairPreferredPermissions.RemoveRange(groupPermissions); + DbContext.Permissions.RemoveRange(individualPermissions); + DbContext.GroupBans.RemoveRange(bannedEntries); + + _lightlessMetrics.IncCounter(MetricsAPI.CounterUsersRegisteredDeleted, 1); + + DbContext.ClientPairs.RemoveRange(otherPairData); + DbContext.Users.Remove(user); + DbContext.Auth.Remove(auth); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + } + + private async Task> GetAllPairedUnpausedUsers(string? uid = null) + { + uid ??= UserUID; + + return (await GetSyncedUnpausedOnlinePairs(UserUID).ConfigureAwait(false)); + } + + 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 pairInfo = await GetAllPairInfo(pair.GroupUserUID).ConfigureAwait(false); + + foreach (var groupUserPair in groupUsers.Where(g => !string.Equals(g.GroupUserUID, pair.GroupUserUID, StringComparison.Ordinal))) + { + await UserGroupLeave(groupUserPair, pairIdent, pairInfo, 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, string userIdent, Dictionary allUserPairs, string? uid = null) + { + uid ??= UserUID; + if (!allUserPairs.TryGetValue(groupUserPair.GroupUserUID, out var info) || !info.IsSynced) + { + 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(LightlessHubLogger.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(LightlessHubLogger.Args(dto, "Deleted")); + + DbContext.Groups.Remove(group); + } + else + { + var groupHasMigrated = await SharedDbFunctions.MigrateOrDeleteGroup(DbContext, group, groupPairsWithoutSelf, _maxExistingGroupsByUser).ConfigureAwait(false); + + if (groupHasMigrated.Item1) + { + _logger.LogCallInfo(LightlessHubLogger.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.ToEnum())).ConfigureAwait(false); + } + else + { + _logger.LogCallInfo(LightlessHubLogger.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(LightlessHubLogger.Args(dto, "Success")); + + await Clients.Users(groupPairsWithoutSelf.Select(p => p.GroupUserUID)).Client_GroupPairLeft(new GroupPairDto(dto.Group, groupPair.GroupUser.ToUserData())).ConfigureAwait(false); + + var ident = await GetUserIdent(userUid).ConfigureAwait(false); + + var pairs = await GetAllPairInfo(userUid).ConfigureAwait(false); + + foreach (var groupUserPair in groupPairsWithoutSelf) + { + await UserGroupLeave(groupUserPair, ident, pairs, userUid).ConfigureAwait(false); + } + } + + private async Task GetPairInfo(string uid, string otheruid) + { + var clientPairs = from cp in DbContext.ClientPairs.AsNoTracking().Where(u => u.UserUID == uid && u.OtherUserUID == otheruid) + join cp2 in DbContext.ClientPairs.AsNoTracking().Where(u => u.OtherUserUID == uid && u.UserUID == otheruid) + on new + { + UserUID = cp.UserUID, + OtherUserUID = cp.OtherUserUID + } + equals new + { + UserUID = cp2.OtherUserUID, + OtherUserUID = cp2.UserUID + } into joined + from c in joined.DefaultIfEmpty() + where cp.UserUID == uid + select new + { + UserUID = cp.UserUID, + OtherUserUID = cp.OtherUserUID, + Gid = string.Empty, + Synced = c != null + }; + + + var groupPairs = from gp in DbContext.GroupPairs.AsNoTracking().Where(u => u.GroupUserUID == uid) + join gp2 in DbContext.GroupPairs.AsNoTracking().Where(u => u.GroupUserUID == otheruid) + on new + { + GID = gp.GroupGID + } + equals new + { + GID = gp2.GroupGID + } + where gp.GroupUserUID == uid + select new + { + UserUID = gp.GroupUserUID, + OtherUserUID = gp2.GroupUserUID, + Gid = Convert.ToString(gp2.GroupGID), + Synced = true + }; + + var allPairs = clientPairs.Concat(groupPairs); + + var result = from user in allPairs + join u in DbContext.Users.AsNoTracking() on user.OtherUserUID equals u.UID + join o in DbContext.Permissions.AsNoTracking().Where(u => u.UserUID == uid) + on new { UserUID = user.UserUID, OtherUserUID = user.OtherUserUID } + equals new { UserUID = o.UserUID, OtherUserUID = o.OtherUserUID } + into ownperms + from ownperm in ownperms.DefaultIfEmpty() + join p in DbContext.Permissions.AsNoTracking().Where(u => u.OtherUserUID == uid) + on new { UserUID = user.OtherUserUID, OtherUserUID = user.UserUID } + equals new { UserUID = p.UserUID, OtherUserUID = p.OtherUserUID } + into otherperms + from otherperm in otherperms.DefaultIfEmpty() + where user.UserUID == uid + && u.UID == user.OtherUserUID + && ownperm.UserUID == user.UserUID && ownperm.OtherUserUID == user.OtherUserUID + && (otherperm == null || (otherperm.OtherUserUID == user.UserUID && otherperm.UserUID == user.OtherUserUID)) + select new + { + UserUID = user.UserUID, + OtherUserUID = user.OtherUserUID, + OtherUserAlias = u.Alias, + GID = user.Gid, + Synced = user.Synced, + OwnPermissions = ownperm, + OtherPermissions = otherperm + }; + + var resultList = await result.AsNoTracking().ToListAsync().ConfigureAwait(false); + + if (!resultList.Any()) return null; + + var groups = resultList.Select(g => g.GID).ToList(); + return new UserInfo(resultList[0].OtherUserAlias, + resultList.SingleOrDefault(p => string.IsNullOrEmpty(p.GID))?.Synced ?? false, + resultList.Max(p => p.Synced), + resultList.Select(p => string.IsNullOrEmpty(p.GID) ? Constants.IndividualKeyword : p.GID).ToList(), + resultList[0].OwnPermissions, + resultList[0].OtherPermissions); + } + + private async Task> GetAllPairInfo(string uid) + { + var clientPairs = from cp in DbContext.ClientPairs.AsNoTracking().Where(u => u.UserUID == uid) + join cp2 in DbContext.ClientPairs.AsNoTracking().Where(u => u.OtherUserUID == uid) + on new + { + UserUID = cp.UserUID, + OtherUserUID = cp.OtherUserUID + } + equals new + { + UserUID = cp2.OtherUserUID, + OtherUserUID = cp2.UserUID + } into joined + from c in joined.DefaultIfEmpty() + where cp.UserUID == uid + select new + { + UserUID = cp.UserUID, + OtherUserUID = cp.OtherUserUID, + Gid = string.Empty, + Synced = c != null + }; + + + var groupPairs = from gp in DbContext.GroupPairs.AsNoTracking().Where(u => u.GroupUserUID == uid) + join gp2 in DbContext.GroupPairs.AsNoTracking().Where(u => u.GroupUserUID != uid) + on new + { + GID = gp.GroupGID + } + equals new + { + GID = gp2.GroupGID + } + select new + { + UserUID = gp.GroupUserUID, + OtherUserUID = gp2.GroupUserUID, + Gid = Convert.ToString(gp2.GroupGID), + Synced = true + }; + + var allPairs = clientPairs.Concat(groupPairs); + + var result = from user in allPairs + join u in DbContext.Users.AsNoTracking() on user.OtherUserUID equals u.UID + join o in DbContext.Permissions.AsNoTracking().Where(u => u.UserUID == uid) + on new { UserUID = user.UserUID, OtherUserUID = user.OtherUserUID } + equals new { UserUID = o.UserUID, OtherUserUID = o.OtherUserUID } + into ownperms + from ownperm in ownperms.DefaultIfEmpty() + join p in DbContext.Permissions.AsNoTracking().Where(u => u.OtherUserUID == uid) + on new { UserUID = user.OtherUserUID, OtherUserUID = user.UserUID } + equals new { UserUID = p.UserUID, OtherUserUID = p.OtherUserUID } + into otherperms + from otherperm in otherperms.DefaultIfEmpty() + where user.UserUID == uid + && u.UID == user.OtherUserUID + && ownperm.UserUID == user.UserUID && ownperm.OtherUserUID == user.OtherUserUID + && (otherperm == null || (otherperm.OtherUserUID == user.UserUID && otherperm.UserUID == user.OtherUserUID)) + select new + { + UserUID = user.UserUID, + OtherUserUID = user.OtherUserUID, + OtherUserAlias = u.Alias, + GID = user.Gid, + Synced = user.Synced, + OwnPermissions = ownperm, + OtherPermissions = otherperm + }; + + var resultList = await result.AsNoTracking().ToListAsync().ConfigureAwait(false); + return resultList.GroupBy(g => g.OtherUserUID, StringComparer.Ordinal).ToDictionary(g => g.Key, g => + { + return new UserInfo(g.First().OtherUserAlias, + g.SingleOrDefault(p => string.IsNullOrEmpty(p.GID))?.Synced ?? false, + g.Max(p => p.Synced), + g.Select(p => string.IsNullOrEmpty(p.GID) ? Constants.IndividualKeyword : p.GID).ToList(), + g.First().OwnPermissions, + g.First().OtherPermissions); + }, StringComparer.Ordinal); + } + + private async Task> GetSyncedUnpausedOnlinePairs(string uid) + { + var clientPairs = from cp in DbContext.ClientPairs.AsNoTracking().Where(u => u.UserUID == uid) + join cp2 in DbContext.ClientPairs.AsNoTracking().Where(u => u.OtherUserUID == uid) + on new + { + UserUID = cp.UserUID, + OtherUserUID = cp.OtherUserUID + } + equals new + { + UserUID = cp2.OtherUserUID, + OtherUserUID = cp2.UserUID + } into joined + from c in joined.DefaultIfEmpty() + where cp.UserUID == uid && c.UserUID != null + select new + { + UserUID = cp.UserUID, + OtherUserUID = cp.OtherUserUID, + }; + + + var groupPairs = from gp in DbContext.GroupPairs.AsNoTracking().Where(u => u.GroupUserUID == uid) + join gp2 in DbContext.GroupPairs.AsNoTracking().Where(u => u.GroupUserUID != uid) + on new + { + GID = gp.GroupGID + } + equals new + { + GID = gp2.GroupGID + } + select new + { + UserUID = gp.GroupUserUID, + OtherUserUID = gp2.GroupUserUID, + }; + + var allPairs = clientPairs.Concat(groupPairs); + + var result = from user in allPairs + join o in DbContext.Permissions.AsNoTracking().Where(u => u.UserUID == uid) + on new { UserUID = user.UserUID, OtherUserUID = user.OtherUserUID } + equals new { UserUID = o.UserUID, OtherUserUID = o.OtherUserUID } + into ownperms + from ownperm in ownperms.DefaultIfEmpty() + join p in DbContext.Permissions.AsNoTracking().Where(u => u.OtherUserUID == uid) + on new { UserUID = user.OtherUserUID, OtherUserUID = user.UserUID } + equals new { UserUID = p.UserUID, OtherUserUID = p.OtherUserUID } + into otherperms + from otherperm in otherperms.DefaultIfEmpty() + where user.UserUID == uid + && ownperm.UserUID == user.UserUID && ownperm.OtherUserUID == user.OtherUserUID + && otherperm.OtherUserUID == user.UserUID && otherperm.UserUID == user.OtherUserUID + && !ownperm.IsPaused && (otherperm == null ? false : !otherperm.IsPaused) + select user.OtherUserUID; + + return await result.Distinct().AsNoTracking().ToListAsync().ConfigureAwait(false); + } + + public record UserInfo(string Alias, bool IndividuallyPaired, bool IsSynced, List GIDs, UserPermissionSet? OwnPermissions, UserPermissionSet? OtherPermissions); +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.GposeLobby.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.GposeLobby.cs new file mode 100644 index 0000000..0fd65bd --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.GposeLobby.cs @@ -0,0 +1,155 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Dto.CharaData; +using LightlessSyncServer.Utils; +using LightlessSyncShared.Metrics; +using LightlessSyncShared.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; + +namespace LightlessSyncServer.Hubs; + +public partial class LightlessHub +{ + 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) + { + _lightlessMetrics.IncGauge(MetricsAPI.GaugeGposeLobbyUsers); + if (priorUsers.Count == 0) + _lightlessMetrics.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); + + _lightlessMetrics.DecGauge(MetricsAPI.GaugeGposeLobbyUsers); + + if (priorUsers.Count == 1) + { + await _redis.RemoveAsync($"GposeLobby:{lobbyId}").ConfigureAwait(false); + _lightlessMetrics.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/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs new file mode 100644 index 0000000..f9895ff --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs @@ -0,0 +1,672 @@ +using LightlessSync.API.Data.Enum; +using LightlessSync.API.Data.Extensions; +using LightlessSync.API.Dto.Group; +using LightlessSyncServer.Utils; +using LightlessSyncShared.Models; +using LightlessSyncShared.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using System.Security.Cryptography; + +namespace LightlessSyncServer.Hubs; + +public partial class LightlessHub +{ + [Authorize(Policy = "Identified")] + public async Task GroupBanUser(GroupPairDto dto, string reason) + { + _logger.LogCallInfo(LightlessHubLogger.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(LightlessHubLogger.Args(dto, "Success")); + } + + [Authorize(Policy = "Identified")] + public async Task GroupChangeGroupPermissionState(GroupPermissionDto dto) + { + _logger.LogCallInfo(LightlessHubLogger.Args(dto)); + + var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false); + if (!hasRights) return; + + group.InvitesEnabled = !dto.Permissions.HasFlag(GroupPermissions.DisableInvites); + group.PreferDisableSounds = dto.Permissions.HasFlag(GroupPermissions.PreferDisableSounds); + group.PreferDisableAnimations = dto.Permissions.HasFlag(GroupPermissions.PreferDisableAnimations); + group.PreferDisableVFX = dto.Permissions.HasFlag(GroupPermissions.PreferDisableVFX); + + 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 GroupChangeOwnership(GroupPairDto dto) + { + _logger.LogCallInfo(LightlessHubLogger.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(LightlessHubLogger.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.ToEnum())).ConfigureAwait(false); + } + + [Authorize(Policy = "Identified")] + public async Task GroupChangePassword(GroupPasswordDto dto) + { + _logger.LogCallInfo(LightlessHubLogger.Args(dto)); + + var (isOwner, group) = await TryValidateOwner(dto.Group.GID).ConfigureAwait(false); + if (!isOwner || dto.Password.Length < 10) return false; + + _logger.LogCallInfo(LightlessHubLogger.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(LightlessHubLogger.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); + var notPinned = groupPairs.Where(g => !g.IsPinned && !g.IsModerator).ToList(); + + await Clients.Users(notPinned.Select(g => g.GroupUserUID)).Client_GroupDelete(new GroupDto(group.ToGroupData())).ConfigureAwait(false); + + _logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success")); + + DbContext.GroupPairs.RemoveRange(notPinned); + + foreach (var pair in notPinned) + { + await Clients.Users(groupPairs.Where(p => p.IsPinned || p.IsModerator).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 GetAllPairInfo(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(pair, pairIdent, allUserPairs, pair.GroupUserUID).ConfigureAwait(false); + } + } + + await DbContext.SaveChangesAsync().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(12); + while (await DbContext.Groups.AnyAsync(g => g.GID == "MSS-" + gid).ConfigureAwait(false)) + { + gid = StringUtils.GenerateRandomString(12); + } + gid = "MSS-" + gid; + + var passwd = StringUtils.GenerateRandomString(16); + using var sha = SHA256.Create(); + var hashedPw = StringUtils.Sha256String(passwd); + + UserDefaultPreferredPermission defaultPermissions = await DbContext.UserDefaultPreferredPermissions.SingleAsync(u => u.UserUID == UserUID).ConfigureAwait(false); + + Group newGroup = new() + { + GID = gid, + HashedPassword = hashedPw, + InvitesEnabled = true, + OwnerUID = UserUID, + PreferDisableAnimations = defaultPermissions.DisableGroupAnimations, + PreferDisableSounds = defaultPermissions.DisableGroupSounds, + PreferDisableVFX = defaultPermissions.DisableGroupVFX + }; + + GroupPair initialPair = new() + { + GroupGID = newGroup.GID, + GroupUserUID = UserUID, + IsPinned = true, + }; + + GroupPairPreferredPermission initialPrefPermissions = new() + { + UserUID = UserUID, + GroupGID = newGroup.GID, + DisableSounds = defaultPermissions.DisableGroupSounds, + DisableAnimations = defaultPermissions.DisableGroupAnimations, + DisableVFX = defaultPermissions.DisableGroupAnimations + }; + + await DbContext.Groups.AddAsync(newGroup).ConfigureAwait(false); + await DbContext.GroupPairs.AddAsync(initialPair).ConfigureAwait(false); + await DbContext.GroupPairPreferredPermissions.AddAsync(initialPrefPermissions).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(), + newGroup.ToEnum(), initialPrefPermissions.ToEnum(), initialPair.ToEnum(), new(StringComparer.Ordinal))) + .ConfigureAwait(false); + + _logger.LogCallInfo(LightlessHubLogger.Args(gid)); + + return new GroupJoinDto(newGroup.ToGroupData(), passwd, initialPrefPermissions.ToEnum()); + } + + [Authorize(Policy = "Identified")] + public async Task> GroupCreateTempInvite(GroupDto dto, int amount) + { + _logger.LogCallInfo(LightlessHubLogger.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(LightlessHubLogger.Args(dto)); + + var (hasRights, group) = await TryValidateOwner(dto.Group.GID).ConfigureAwait(false); + + _logger.LogCallInfo(LightlessHubLogger.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(LightlessHubLogger.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(LightlessHubLogger.Args(dto, bannedGroupUsers.Count)); + + return bannedGroupUsers; + } + + [Authorize(Policy = "Identified")] + public async Task GroupJoin(GroupPasswordDto dto) + { + var aliasOrGid = dto.Group.GID.Trim(); + + _logger.LogCallInfo(LightlessHubLogger.Args(dto)); + + 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 new GroupJoinInfoDto(null, null, GroupPermissions.NoneSet, false); + + return new GroupJoinInfoDto(group.ToGroupData(), group.Owner.ToUserData(), group.ToEnum(), true); + } + + [Authorize(Policy = "Identified")] + public async Task GroupJoinFinalize(GroupJoinDto dto) + { + var aliasOrGid = dto.Group.GID.Trim(); + + _logger.LogCallInfo(LightlessHubLogger.Args(dto)); + + 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; + + // get all pairs before we join + var allUserPairs = (await GetAllPairInfo(UserUID).ConfigureAwait(false)); + + if (oneTimeInvite != null) + { + _logger.LogCallInfo(LightlessHubLogger.Args(aliasOrGid, "TempInvite", oneTimeInvite.Invite)); + DbContext.Remove(oneTimeInvite); + } + + GroupPair newPair = new() + { + GroupGID = group.GID, + GroupUserUID = UserUID, + }; + + var preferredPermissions = await DbContext.GroupPairPreferredPermissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.GroupGID == group.GID).ConfigureAwait(false); + if (preferredPermissions == null) + { + GroupPairPreferredPermission newPerms = new() + { + GroupGID = group.GID, + UserUID = UserUID, + DisableSounds = dto.GroupUserPreferredPermissions.IsDisableSounds(), + DisableVFX = dto.GroupUserPreferredPermissions.IsDisableVFX(), + DisableAnimations = dto.GroupUserPreferredPermissions.IsDisableAnimations(), + IsPaused = false + }; + + DbContext.Add(newPerms); + preferredPermissions = newPerms; + } + else + { + preferredPermissions.DisableSounds = dto.GroupUserPreferredPermissions.IsDisableSounds(); + preferredPermissions.DisableVFX = dto.GroupUserPreferredPermissions.IsDisableVFX(); + preferredPermissions.DisableAnimations = dto.GroupUserPreferredPermissions.IsDisableAnimations(); + preferredPermissions.IsPaused = false; + DbContext.Update(preferredPermissions); + } + + await DbContext.GroupPairs.AddAsync(newPair).ConfigureAwait(false); + + _logger.LogCallInfo(LightlessHubLogger.Args(aliasOrGid, "Success")); + + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + var groupInfos = await DbContext.GroupPairs.Where(u => u.GroupGID == group.GID && (u.IsPinned || u.IsModerator)).ToListAsync().ConfigureAwait(false); + await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(group.ToGroupData(), group.Owner.ToUserData(), + group.ToEnum(), preferredPermissions.ToEnum(), newPair.ToEnum(), + groupInfos.ToDictionary(u => u.GroupUserUID, u => u.ToEnum(), StringComparer.Ordinal))).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); + + var userPairsAfterJoin = await GetAllPairInfo(UserUID).ConfigureAwait(false); + + foreach (var pair in groupPairs) + { + var perms = userPairsAfterJoin.TryGetValue(pair.GroupUserUID, out var userinfo); + // check if we have had prior permissions to that pair, if not add them + var ownPermissionsToOther = userinfo?.OwnPermissions ?? null; + if (ownPermissionsToOther == null) + { + var existingPermissionsOnDb = await DbContext.Permissions.SingleOrDefaultAsync(p => p.UserUID == UserUID && p.OtherUserUID == pair.GroupUserUID).ConfigureAwait(false); + + if (existingPermissionsOnDb == null) + { + ownPermissionsToOther = new() + { + UserUID = UserUID, + OtherUserUID = pair.GroupUserUID, + DisableAnimations = preferredPermissions.DisableAnimations, + DisableSounds = preferredPermissions.DisableSounds, + DisableVFX = preferredPermissions.DisableVFX, + IsPaused = preferredPermissions.IsPaused, + Sticky = false + }; + + await DbContext.Permissions.AddAsync(ownPermissionsToOther).ConfigureAwait(false); + } + else + { + existingPermissionsOnDb.DisableAnimations = preferredPermissions.DisableAnimations; + existingPermissionsOnDb.DisableSounds = preferredPermissions.DisableSounds; + existingPermissionsOnDb.DisableVFX = preferredPermissions.DisableVFX; + existingPermissionsOnDb.IsPaused = false; + existingPermissionsOnDb.Sticky = false; + + DbContext.Update(existingPermissionsOnDb); + + ownPermissionsToOther = existingPermissionsOnDb; + } + } + else if (!ownPermissionsToOther.Sticky) + { + ownPermissionsToOther = await DbContext.Permissions.SingleAsync(u => u.UserUID == UserUID && u.OtherUserUID == pair.GroupUserUID).ConfigureAwait(false); + + // update the existing permission only if it was not set to sticky + ownPermissionsToOther.DisableAnimations = preferredPermissions.DisableAnimations; + ownPermissionsToOther.DisableVFX = preferredPermissions.DisableVFX; + ownPermissionsToOther.DisableSounds = preferredPermissions.DisableSounds; + ownPermissionsToOther.IsPaused = false; + + DbContext.Update(ownPermissionsToOther); + } + + // get others permissionset to self and eventually update it + var otherPermissionToSelf = userinfo?.OtherPermissions ?? null; + if (otherPermissionToSelf == null) + { + var otherExistingPermsOnDb = await DbContext.Permissions.SingleOrDefaultAsync(p => p.UserUID == pair.GroupUserUID && p.OtherUserUID == UserUID).ConfigureAwait(false); + + if (otherExistingPermsOnDb == null) + { + var otherPreferred = await DbContext.GroupPairPreferredPermissions.SingleAsync(u => u.GroupGID == group.GID && u.UserUID == pair.GroupUserUID).ConfigureAwait(false); + otherExistingPermsOnDb = new() + { + UserUID = pair.GroupUserUID, + OtherUserUID = UserUID, + DisableAnimations = otherPreferred.DisableAnimations, + DisableSounds = otherPreferred.DisableSounds, + DisableVFX = otherPreferred.DisableVFX, + IsPaused = otherPreferred.IsPaused, + Sticky = false + }; + + await DbContext.AddAsync(otherExistingPermsOnDb).ConfigureAwait(false); + } + else if (!otherExistingPermsOnDb.Sticky) + { + var otherPreferred = await DbContext.GroupPairPreferredPermissions.SingleAsync(u => u.GroupGID == group.GID && u.UserUID == pair.GroupUserUID).ConfigureAwait(false); + otherExistingPermsOnDb.DisableAnimations = otherPreferred.DisableAnimations; + otherExistingPermsOnDb.DisableSounds = otherPreferred.DisableSounds; + otherExistingPermsOnDb.DisableVFX = otherPreferred.DisableVFX; + otherExistingPermsOnDb.IsPaused = otherPreferred.IsPaused; + + DbContext.Update(otherExistingPermsOnDb); + } + + otherPermissionToSelf = otherExistingPermsOnDb; + } + else if (!otherPermissionToSelf.Sticky) + { + var otherPreferred = await DbContext.GroupPairPreferredPermissions.SingleAsync(u => u.GroupGID == group.GID && u.UserUID == pair.GroupUserUID).ConfigureAwait(false); + otherPermissionToSelf.DisableAnimations = otherPreferred.DisableAnimations; + otherPermissionToSelf.DisableSounds = otherPreferred.DisableSounds; + otherPermissionToSelf.DisableVFX = otherPreferred.DisableVFX; + otherPermissionToSelf.IsPaused = otherPreferred.IsPaused; + + DbContext.Update(otherPermissionToSelf); + } + + await Clients.User(UserUID).Client_GroupPairJoined(new GroupPairFullInfoDto(group.ToGroupData(), + pair.ToUserData(), ownPermissionsToOther.ToUserPermissions(setSticky: ownPermissionsToOther.Sticky), + otherPermissionToSelf.ToUserPermissions(setSticky: false))).ConfigureAwait(false); + await Clients.User(pair.GroupUserUID).Client_GroupPairJoined(new GroupPairFullInfoDto(group.ToGroupData(), + self.ToUserData(), otherPermissionToSelf.ToUserPermissions(setSticky: otherPermissionToSelf.Sticky), + ownPermissionsToOther.ToUserPermissions(setSticky: false))).ConfigureAwait(false); + + // if not paired prior and neither has the permissions set to paused, send online + if ((!allUserPairs.ContainsKey(pair.GroupUserUID) || (allUserPairs.TryGetValue(pair.GroupUserUID, out var info) && !info.IsSynced)) + && !otherPermissionToSelf.IsPaused && !ownPermissionsToOther.IsPaused) + { + var groupUserIdent = await GetUserIdent(pair.GroupUserUID).ConfigureAwait(false); + if (!string.IsNullOrEmpty(groupUserIdent)) + { + await Clients.User(UserUID).Client_UserSendOnline(new(pair.ToUserData(), groupUserIdent)).ConfigureAwait(false); + await Clients.User(pair.GroupUserUID).Client_UserSendOnline(new(self.ToUserData(), UserCharaIdent)).ConfigureAwait(false); + } + } + } + + await DbContext.SaveChangesAsync().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(LightlessHubLogger.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(LightlessHubLogger.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(LightlessHubLogger.Args(dto, "Success")); + + DbContext.GroupPairs.Remove(groupPair); + + 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) + { + await DbContext.SaveChangesAsync().ConfigureAwait(false); + return; + } + + await Clients.User(dto.User.UID).Client_GroupDelete(new GroupDto(dto.Group)).ConfigureAwait(false); + + var userPairs = await GetAllPairInfo(dto.User.UID).ConfigureAwait(false); + foreach (var groupUserPair in groupPairs) + { + await UserGroupLeave(groupUserPair, userIdent, userPairs, dto.User.UID).ConfigureAwait(false); + } + } + + [Authorize(Policy = "Identified")] + public async Task GroupSetUserInfo(GroupPairUserInfoDto dto) + { + _logger.LogCallInfo(LightlessHubLogger.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(GroupPairUserInfo.IsPinned) && userIsModerator && !userPair.IsPinned) + { + userPair.IsPinned = true; + } + else if (userIsModerator && userPair.IsPinned) + { + userPair.IsPinned = false; + } + + if (dto.GroupUserInfo.HasFlag(GroupPairUserInfo.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.ToEnum())).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); + var preferredPermissions = (await DbContext.GroupPairPreferredPermissions.Where(u => u.UserUID == UserUID).ToListAsync().ConfigureAwait(false)) + .Where(u => groups.Exists(k => string.Equals(k.GroupGID, u.GroupGID, StringComparison.Ordinal))) + .ToDictionary(u => groups.First(f => string.Equals(f.GroupGID, u.GroupGID, StringComparison.Ordinal)), u => u); + var groupInfos = await DbContext.GroupPairs.Where(u => groups.Select(g => g.GroupGID).Contains(u.GroupGID) && (u.IsPinned || u.IsModerator)) + .ToListAsync().ConfigureAwait(false); + + return preferredPermissions.Select(g => new GroupFullInfoDto(g.Key.Group.ToGroupData(), g.Key.Group.Owner.ToUserData(), + g.Key.Group.ToEnum(), g.Value.ToEnum(), g.Key.ToEnum(), + groupInfos.Where(i => string.Equals(i.GroupGID, g.Key.GroupGID, StringComparison.Ordinal)) + .ToDictionary(i => i.GroupUserUID, i => i.ToEnum(), StringComparer.Ordinal))).ToList(); + } + + [Authorize(Policy = "Identified")] + public async Task GroupUnbanUser(GroupPairDto dto) + { + _logger.LogCallInfo(LightlessHubLogger.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(LightlessHubLogger.Args(dto, "Success")); + } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Permissions.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Permissions.cs new file mode 100644 index 0000000..f4b7687 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Permissions.cs @@ -0,0 +1,172 @@ +using LightlessSync.API.Data.Enum; +using LightlessSync.API.Dto; +using LightlessSync.API.Dto.Group; +using LightlessSync.API.Dto.User; +using LightlessSync.API.Data.Extensions; +using LightlessSyncServer.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; + +namespace LightlessSyncServer.Hubs; + +public partial class LightlessHub +{ + [Authorize(Policy = "Authenticated")] + public async Task UserUpdateDefaultPermissions(DefaultPermissionsDto defaultPermissions) + { + _logger.LogCallInfo(LightlessHubLogger.Args(defaultPermissions)); + + var permissions = await DbContext.UserDefaultPreferredPermissions.SingleAsync(u => u.UserUID == UserUID).ConfigureAwait(false); + + permissions.DisableGroupAnimations = defaultPermissions.DisableGroupAnimations; + permissions.DisableGroupSounds = defaultPermissions.DisableGroupSounds; + permissions.DisableGroupVFX = defaultPermissions.DisableGroupVFX; + permissions.DisableIndividualAnimations = defaultPermissions.DisableIndividualAnimations; + permissions.DisableIndividualSounds = defaultPermissions.DisableIndividualSounds; + permissions.DisableIndividualVFX = defaultPermissions.DisableIndividualVFX; + permissions.IndividualIsSticky = defaultPermissions.IndividualIsSticky; + + DbContext.Update(permissions); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + await Clients.Caller.Client_UserUpdateDefaultPermissions(defaultPermissions).ConfigureAwait(false); + } + + [Authorize(Policy = "Identified")] + public async Task SetBulkPermissions(BulkPermissionsDto dto) + { + _logger.LogCallInfo(LightlessHubLogger.Args( + "Individual", string.Join(';', dto.AffectedUsers.Select(g => g.Key + ":" + g.Value)), + "Group", string.Join(';', dto.AffectedGroups.Select(g => g.Key + ":" + g.Value)))); + + // remove self + dto.AffectedUsers.Remove(UserUID, out _); + if (!dto.AffectedUsers.Any() && !dto.AffectedGroups.Any()) return; + + // get all current pairs in any form + var allUsers = await GetAllPairInfo(UserUID).ConfigureAwait(false); + var ownDefaultPerms = await DbContext.UserDefaultPreferredPermissions.SingleAsync(u => u.UserUID == UserUID).ConfigureAwait(false); + + foreach (var user in dto.AffectedUsers) + { + bool setSticky = false; + var newPerm = user.Value; + if (!allUsers.TryGetValue(user.Key, out var pairData)) continue; + if (!pairData.OwnPermissions.Sticky && !newPerm.IsSticky()) + { + setSticky = ownDefaultPerms.IndividualIsSticky; + } + + var pauseChange = pairData.OwnPermissions.IsPaused != newPerm.IsPaused(); + var prevPermissions = await DbContext.Permissions.SingleAsync(u => u.UserUID == UserUID && u.OtherUserUID == user.Key).ConfigureAwait(false); + + prevPermissions.IsPaused = newPerm.IsPaused(); + prevPermissions.DisableAnimations = newPerm.IsDisableAnimations(); + prevPermissions.DisableSounds = newPerm.IsDisableSounds(); + prevPermissions.DisableVFX = newPerm.IsDisableVFX(); + prevPermissions.Sticky = newPerm.IsSticky() || setSticky; + DbContext.Update(prevPermissions); + + // send updated data to pair + var permCopy = newPerm; + permCopy.SetSticky(newPerm.IsSticky() || setSticky); + var permToOther = permCopy; + permToOther.SetSticky(false); + + await Clients.User(UserUID).Client_UserUpdateSelfPairPermissions(new(new(user.Key), permCopy)).ConfigureAwait(false); + if (pairData.OtherPermissions == null) continue; + + await Clients.User(user.Key).Client_UserUpdateOtherPairPermissions(new(new(UserUID), permToOther)).ConfigureAwait(false); + + // check if pause change and send online or offline respectively + if (pauseChange && !pairData.OtherPermissions.IsPaused) + { + var otherCharaIdent = await GetUserIdent(user.Key).ConfigureAwait(false); + + if (UserCharaIdent == null || otherCharaIdent == null) continue; + + if (newPerm.IsPaused()) + { + await Clients.User(UserUID).Client_UserSendOffline(new(new(user.Key))).ConfigureAwait(false); + await Clients.User(user.Key).Client_UserSendOffline(new(new(UserUID))).ConfigureAwait(false); + } + else + { + await Clients.User(UserUID).Client_UserSendOnline(new(new(user.Key), otherCharaIdent)).ConfigureAwait(false); + await Clients.User(user.Key).Client_UserSendOnline(new(new(UserUID), UserCharaIdent)).ConfigureAwait(false); + } + } + } + + foreach (var group in dto.AffectedGroups) + { + var (inGroup, groupPair) = await TryValidateUserInGroup(group.Key).ConfigureAwait(false); + if (!inGroup) continue; + + var groupPreferredPermissions = await DbContext.GroupPairPreferredPermissions + .SingleAsync(u => u.UserUID == UserUID && u.GroupGID == group.Key).ConfigureAwait(false); + + var wasPaused = groupPreferredPermissions.IsPaused; + groupPreferredPermissions.DisableSounds = group.Value.IsDisableSounds(); + groupPreferredPermissions.DisableAnimations = group.Value.IsDisableAnimations(); + groupPreferredPermissions.IsPaused = group.Value.IsPaused(); + groupPreferredPermissions.DisableVFX = group.Value.IsDisableVFX(); + + var nonStickyPairs = allUsers.Where(u => !u.Value.OwnPermissions.Sticky).ToList(); + var affectedGroupPairs = nonStickyPairs.Where(u => u.Value.GIDs.Contains(group.Key, StringComparer.Ordinal)).ToList(); + var groupUserUids = affectedGroupPairs.Select(g => g.Key).ToList(); + var affectedPerms = await DbContext.Permissions.Where(u => u.UserUID == UserUID + && groupUserUids.Any(c => c == u.OtherUserUID)) + .ToListAsync().ConfigureAwait(false); + + foreach (var perm in affectedPerms) + { + perm.DisableSounds = groupPreferredPermissions.DisableSounds; + perm.DisableAnimations = groupPreferredPermissions.DisableAnimations; + perm.IsPaused = groupPreferredPermissions.IsPaused; + perm.DisableVFX = groupPreferredPermissions.DisableVFX; + } + + UserPermissions permissions = UserPermissions.NoneSet; + permissions.SetPaused(groupPreferredPermissions.IsPaused); + permissions.SetDisableAnimations(groupPreferredPermissions.DisableAnimations); + permissions.SetDisableSounds(groupPreferredPermissions.DisableSounds); + permissions.SetDisableVFX(groupPreferredPermissions.DisableVFX); + + await Clients.Users(affectedGroupPairs + .Select(k => k.Key)) + .Client_UserUpdateOtherPairPermissions(new(new(UserUID), permissions)).ConfigureAwait(false); + + await Clients.User(UserUID).Client_GroupChangeUserPairPermissions(new GroupPairUserPermissionDto(new(group.Key), new(UserUID), group.Value)).ConfigureAwait(false); + foreach (var item in affectedGroupPairs.Select(k => k.Key)) + { + await Clients.User(UserUID).Client_UserUpdateSelfPairPermissions(new(new(item), permissions)).ConfigureAwait(false); + } + + if (wasPaused == groupPreferredPermissions.IsPaused) continue; + + foreach (var groupUserPair in affectedGroupPairs) + { + var groupUserIdent = await GetUserIdent(groupUserPair.Key).ConfigureAwait(false); + if (!string.IsNullOrEmpty(groupUserIdent) && !groupUserPair.Value.OtherPermissions.IsPaused) + { + // if we changed to paused and other was not paused before, we send offline + if (groupPreferredPermissions.IsPaused) + { + await Clients.User(UserUID).Client_UserSendOffline(new(new(groupUserPair.Key, groupUserPair.Value.Alias))).ConfigureAwait(false); + await Clients.User(groupUserPair.Key).Client_UserSendOffline(new(new(UserUID))).ConfigureAwait(false); + } + // if we changed to unpaused and other was not paused either we send online + else + { + await Clients.User(UserUID).Client_UserSendOnline(new(new(groupUserPair.Key, groupUserPair.Value.Alias), groupUserIdent)).ConfigureAwait(false); + await Clients.User(groupUserPair.Key).Client_UserSendOnline(new(new(UserUID), UserCharaIdent)).ConfigureAwait(false); + } + } + } + } + + await DbContext.SaveChangesAsync().ConfigureAwait(false); + } +} diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.User.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.User.cs new file mode 100644 index 0000000..53162a2 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.User.cs @@ -0,0 +1,437 @@ +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using LightlessSync.API.Data; +using LightlessSync.API.Data.Enum; +using LightlessSync.API.Data.Extensions; +using LightlessSync.API.Dto.User; +using LightlessSyncServer.Utils; +using LightlessSyncShared.Metrics; +using LightlessSyncShared.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace LightlessSyncServer.Hubs; + +public partial class LightlessHub +{ + 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(LightlessHubLogger.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(LightlessHubLogger.Args(dto, "Success")); + + ClientPair wl = new ClientPair() + { + OtherUser = otherUser, + User = user, + }; + await DbContext.ClientPairs.AddAsync(wl).ConfigureAwait(false); + + var existingData = await GetPairInfo(UserUID, otherUser.UID).ConfigureAwait(false); + + var permissions = existingData?.OwnPermissions; + if (permissions == null || !permissions.Sticky) + { + var ownDefaultPermissions = await DbContext.UserDefaultPreferredPermissions.AsNoTracking().SingleOrDefaultAsync(f => f.UserUID == UserUID).ConfigureAwait(false); + + permissions = new UserPermissionSet() + { + User = user, + OtherUser = otherUser, + DisableAnimations = ownDefaultPermissions.DisableIndividualAnimations, + DisableSounds = ownDefaultPermissions.DisableIndividualSounds, + DisableVFX = ownDefaultPermissions.DisableIndividualVFX, + IsPaused = false, + Sticky = true + }; + + var existingDbPerms = await DbContext.Permissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.OtherUserUID == otherUser.UID).ConfigureAwait(false); + if (existingDbPerms == null) + { + await DbContext.Permissions.AddAsync(permissions).ConfigureAwait(false); + } + else + { + existingDbPerms.DisableAnimations = permissions.DisableAnimations; + existingDbPerms.DisableSounds = permissions.DisableSounds; + existingDbPerms.DisableVFX = permissions.DisableVFX; + existingDbPerms.IsPaused = false; + existingDbPerms.Sticky = true; + + DbContext.Permissions.Update(existingDbPerms); + } + } + + 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 otherPermissions = existingData?.OtherPermissions ?? null; + + var ownPerm = permissions.ToUserPermissions(setSticky: true); + var otherPerm = otherPermissions.ToUserPermissions(); + + var userPairResponse = new UserPairDto(otherUser.ToUserData(), + otherEntry == null ? IndividualPairStatus.OneSided : IndividualPairStatus.Bidirectional, + 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_UserUpdateOtherPairPermissions(new UserPermissionsDto(user.ToUserData(), + permissions.ToUserPermissions())).ConfigureAwait(false); + + await Clients.User(otherUser.UID) + .Client_UpdateUserIndividualPairStatusDto(new(user.ToUserData(), IndividualPairStatus.Bidirectional)) + .ConfigureAwait(false); + + if (!ownPerm.IsPaused() && !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(CensusDataDto? censusData) + { + _logger.LogCallInfo(); + + var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false); + var pairs = await GetOnlineUsers(allPairedUsers).ConfigureAwait(false); + + await SendOnlineToAllPairedUsers().ConfigureAwait(false); + + _lightlessCensus.PublishStatistics(UserUID, censusData); + + return pairs.Select(p => new OnlineUserIdentDto(new UserData(p.Key), p.Value)).ToList(); + } + + [Authorize(Policy = "Identified")] + public async Task> UserGetPairedClients() + { + _logger.LogCallInfo(); + + var pairs = await GetAllPairInfo(UserUID).ConfigureAwait(false); + return pairs.Select(p => + { + return new UserFullPairDto(new UserData(p.Key, p.Value.Alias), + p.Value.ToIndividualPairStatus(), + p.Value.GIDs.Where(g => !string.Equals(g, Constants.IndividualKeyword, StringComparison.OrdinalIgnoreCase)).ToList(), + p.Value.OwnPermissions.ToUserPermissions(setSticky: true), + p.Value.OtherPermissions.ToUserPermissions()); + }).ToList(); + } + + [Authorize(Policy = "Identified")] + public async Task UserGetProfile(UserDto user) + { + _logger.LogCallInfo(LightlessHubLogger.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(LightlessHubLogger.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(LightlessHubLogger.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 recipientUids = dto.Recipients.Select(r => r.UID).ToList(); + bool allCached = await _onlineSyncedPairCacheService.AreAllPlayersCached(UserUID, + recipientUids, Context.ConnectionAborted).ConfigureAwait(false); + + if (!allCached) + { + var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false); + + recipientUids = allPairedUsers.Where(f => recipientUids.Contains(f, StringComparer.Ordinal)).ToList(); + + await _onlineSyncedPairCacheService.CachePlayers(UserUID, allPairedUsers, Context.ConnectionAborted).ConfigureAwait(false); + } + + _logger.LogCallInfo(LightlessHubLogger.Args(recipientUids.Count)); + + await Clients.Users(recipientUids).Client_UserReceiveCharacterData(new OnlineUserCharaDataDto(new UserData(UserUID), dto.CharaData)).ConfigureAwait(false); + + _lightlessCensus.PublishStatistics(UserUID, dto.CensusDataDto); + + _lightlessMetrics.IncCounter(MetricsAPI.CounterUserPushData); + _lightlessMetrics.IncCounter(MetricsAPI.CounterUserPushDataTo, recipientUids.Count); + } + + [Authorize(Policy = "Identified")] + public async Task UserRemovePair(UserDto dto) + { + _logger.LogCallInfo(LightlessHubLogger.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; + + var pairData = await GetPairInfo(UserUID, dto.User.UID).ConfigureAwait(false); + + // delete from database, send update info to users pair list + DbContext.ClientPairs.Remove(callerPair); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + _logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success")); + + await Clients.User(UserUID).Client_UserRemoveClientPair(dto).ConfigureAwait(false); + + // check if opposite entry exists + if (!pairData.IndividuallyPaired) 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; + + // if the other user had paused the user the state will be offline for either, do nothing + bool callerHadPaused = pairData.OwnPermissions?.IsPaused ?? false; + + // send updated individual pair status + await Clients.User(dto.User.UID) + .Client_UpdateUserIndividualPairStatusDto(new(new(UserUID), IndividualPairStatus.OneSided)) + .ConfigureAwait(false); + + UserPermissionSet? otherPermissions = pairData.OtherPermissions; + bool otherHadPaused = otherPermissions?.IsPaused ?? true; + + // if the either had paused, do nothing + if (callerHadPaused && otherHadPaused) return; + + var currentPairData = await GetPairInfo(UserUID, dto.User.UID).ConfigureAwait(false); + + // if neither user had paused each other and either is not in an unpaused group with each other, change state to offline + if (!currentPairData?.IsSynced ?? true) + { + await Clients.User(UserUID).Client_UserSendOffline(dto).ConfigureAwait(false); + await Clients.User(dto.User.UID).Client_UserSendOffline(new(new(UserUID))).ConfigureAwait(false); + } + } + + [Authorize(Policy = "Identified")] + public async Task UserSetProfile(UserProfileDto dto) + { + _logger.LogCallInfo(LightlessHubLogger.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/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.cs new file mode 100644 index 0000000..adf1fe0 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.cs @@ -0,0 +1,212 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Data.Enum; +using LightlessSync.API.Dto; +using LightlessSync.API.SignalR; +using LightlessSyncServer.Services; +using LightlessSyncServer.Utils; +using LightlessSyncShared; +using LightlessSyncShared.Data; +using LightlessSyncShared.Metrics; +using LightlessSyncShared.Models; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils.Configuration; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using StackExchange.Redis.Extensions.Core.Abstractions; +using System.Collections.Concurrent; + +namespace LightlessSyncServer.Hubs; + +[Authorize(Policy = "Authenticated")] +public partial class LightlessHub : Hub, ILightlessHub +{ + private static readonly ConcurrentDictionary _userConnections = new(StringComparer.Ordinal); + private readonly LightlessMetrics _lightlessMetrics; + private readonly SystemInfoService _systemInfoService; + private readonly IHttpContextAccessor _contextAccessor; + private readonly LightlessHubLogger _logger; + private readonly string _shardName; + private readonly int _maxExistingGroupsByUser; + private readonly int _maxJoinedGroupsByUser; + private readonly int _maxGroupUserCount; + private readonly IRedisDatabase _redis; + private readonly OnlineSyncedPairCacheService _onlineSyncedPairCacheService; + private readonly LightlessCensus _lightlessCensus; + private readonly GPoseLobbyDistributionService _gPoseLobbyDistributionService; + private readonly Uri _fileServerAddress; + private readonly Version _expectedClientVersion; + private readonly Lazy _dbContextLazy; + private LightlessDbContext DbContext => _dbContextLazy.Value; + private readonly int _maxCharaDataByUser; + private readonly int _maxCharaDataByUserVanity; + + public LightlessHub(LightlessMetrics lightlessMetrics, + IDbContextFactory lightlessDbContextFactory, ILogger logger, SystemInfoService systemInfoService, + IConfigurationService configuration, IHttpContextAccessor contextAccessor, + IRedisDatabase redisDb, OnlineSyncedPairCacheService onlineSyncedPairCacheService, LightlessCensus lightlessCensus, + GPoseLobbyDistributionService gPoseLobbyDistributionService) + { + _lightlessMetrics = lightlessMetrics; + _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); + _maxCharaDataByUserVanity = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxCharaDataByUserVanity), 50); + _contextAccessor = contextAccessor; + _redis = redisDb; + _onlineSyncedPairCacheService = onlineSyncedPairCacheService; + _lightlessCensus = lightlessCensus; + _gPoseLobbyDistributionService = gPoseLobbyDistributionService; + _logger = new LightlessHubLogger(this, logger); + _dbContextLazy = new Lazy(() => lightlessDbContextFactory.CreateDbContext()); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_dbContextLazy.IsValueCreated) DbContext.Dispose(); + } + + base.Dispose(disposing); + } + + [Authorize(Policy = "Identified")] + public async Task GetConnectionDto() + { + _logger.LogCallInfo(); + + _lightlessMetrics.IncCounter(MetricsAPI.CounterInitializedConnections); + + await Clients.Caller.Client_UpdateSystemInfo(_systemInfoService.SystemInfoDto).ConfigureAwait(false); + + var dbUser = await DbContext.Users.SingleAsync(f => f.UID == UserUID).ConfigureAwait(false); + dbUser.LastLoggedIn = DateTime.UtcNow; + + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, "Welcome to Lightless Sync \"" + _shardName + "\", Current Online Users: " + _systemInfoService.SystemInfoDto.OnlineUsers).ConfigureAwait(false); + + var defaultPermissions = await DbContext.UserDefaultPreferredPermissions.SingleOrDefaultAsync(u => u.UserUID == UserUID).ConfigureAwait(false); + if (defaultPermissions == null) + { + defaultPermissions = new UserDefaultPreferredPermission() + { + UserUID = UserUID, + }; + + DbContext.UserDefaultPreferredPermissions.Add(defaultPermissions); + } + + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + return new ConnectionDto(new UserData(dbUser.UID, string.IsNullOrWhiteSpace(dbUser.Alias) ? null : dbUser.Alias)) + { + CurrentClientVersion = _expectedClientVersion, + ServerVersion = ILightlessHub.ApiVersion, + IsAdmin = dbUser.IsAdmin, + IsModerator = dbUser.IsModerator, + ServerInfo = new ServerInfo() + { + MaxGroupsCreatedByUser = _maxExistingGroupsByUser, + ShardName = _shardName, + MaxGroupsJoinedByUser = _maxJoinedGroupsByUser, + MaxGroupUserCount = _maxGroupUserCount, + FileServerAddress = _fileServerAddress, + MaxCharaData = _maxCharaDataByUser, + MaxCharaDataVanity = _maxCharaDataByUserVanity, + }, + DefaultPreferredPermissions = new DefaultPermissionsDto() + { + DisableGroupAnimations = defaultPermissions.DisableGroupAnimations, + DisableGroupSounds = defaultPermissions.DisableGroupSounds, + DisableGroupVFX = defaultPermissions.DisableGroupVFX, + DisableIndividualAnimations = defaultPermissions.DisableIndividualAnimations, + DisableIndividualSounds = defaultPermissions.DisableIndividualSounds, + DisableIndividualVFX = defaultPermissions.DisableIndividualVFX, + IndividualIsSticky = defaultPermissions.IndividualIsSticky, + }, + }; + } + + [Authorize(Policy = "Authenticated")] + public async Task CheckClientHealth() + { + await UpdateUserOnRedis().ConfigureAwait(false); + + return false; + } + + [Authorize(Policy = "Authenticated")] + public override async Task OnConnectedAsync() + { + if (_userConnections.TryGetValue(UserUID, out var oldId)) + { + _logger.LogCallWarning(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), "UpdatingId", oldId, Context.ConnectionId)); + _userConnections[UserUID] = Context.ConnectionId; + } + else + { + _lightlessMetrics.IncGaugeWithLabels(MetricsAPI.GaugeConnections, labels: Continent); + + try + { + _logger.LogCallInfo(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), Context.ConnectionId, UserCharaIdent)); + await _onlineSyncedPairCacheService.InitPlayer(UserUID).ConfigureAwait(false); + await UpdateUserOnRedis().ConfigureAwait(false); + _userConnections[UserUID] = Context.ConnectionId; + } + catch + { + _userConnections.Remove(UserUID, out _); + } + } + + await base.OnConnectedAsync().ConfigureAwait(false); + } + + [Authorize(Policy = "Authenticated")] + public override async Task OnDisconnectedAsync(Exception exception) + { + if (_userConnections.TryGetValue(UserUID, out var connectionId) + && string.Equals(connectionId, Context.ConnectionId, StringComparison.Ordinal)) + { + _lightlessMetrics.DecGaugeWithLabels(MetricsAPI.GaugeConnections, labels: Continent); + + try + { + await GposeLobbyLeave().ConfigureAwait(false); + + await _onlineSyncedPairCacheService.DisposePlayer(UserUID).ConfigureAwait(false); + + _logger.LogCallInfo(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), Context.ConnectionId, UserCharaIdent)); + if (exception != null) + _logger.LogCallWarning(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), Context.ConnectionId, exception.Message, exception.StackTrace)); + + await RemoveUserFromRedis().ConfigureAwait(false); + + _lightlessCensus.ClearStatistics(UserUID); + + await SendOfflineToAllPairedUsers().ConfigureAwait(false); + + DbContext.RemoveRange(DbContext.Files.Where(f => !f.Uploaded && f.UploaderUID == UserUID)); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + } + catch { } + finally + { + _userConnections.Remove(UserUID, out _); + } + } + else + { + _logger.LogCallWarning(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), "ObsoleteId", UserUID, Context.ConnectionId)); + } + + await base.OnDisconnectedAsync(exception).ConfigureAwait(false); + } +} diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/SignalRLimitFilter.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/SignalRLimitFilter.cs new file mode 100644 index 0000000..89c5935 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/SignalRLimitFilter.cs @@ -0,0 +1,112 @@ +using AspNetCoreRateLimit; +using LightlessSyncShared; +using LightlessSyncShared.Utils; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Options; + +namespace LightlessSyncServer.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, LightlessClaimTypes.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/LightlessSyncServer/LightlessSyncServer/LightlessSyncServer.csproj b/LightlessSyncServer/LightlessSyncServer/LightlessSyncServer.csproj new file mode 100644 index 0000000..2891a14 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/LightlessSyncServer.csproj @@ -0,0 +1,41 @@ + + + + net9.0 + aspnet-LightlessSyncServer-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/LightlessSyncServer/LightlessSyncServer/Program.cs b/LightlessSyncServer/LightlessSyncServer/Program.cs new file mode 100644 index 0000000..426d520 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Program.cs @@ -0,0 +1,96 @@ +using Microsoft.EntityFrameworkCore; +using LightlessSyncShared.Data; +using LightlessSyncShared.Metrics; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils.Configuration; + +namespace LightlessSyncServer; + +public class Program +{ + public static void Main(string[] args) + { + var hostBuilder = CreateHostBuilder(args); + using var host = hostBuilder.Build(); + using (var scope = host.Services.CreateScope()) + { + var services = scope.ServiceProvider; + var factory = services.GetRequiredService>(); + using var context = factory.CreateDbContext(); + var options = services.GetRequiredService>(); + var logger = host.Services.GetRequiredService>(); + + if (options.IsMain) + { + context.Database.SetCommandTimeout(TimeSpan.FromMinutes(10)); + context.Database.Migrate(); + context.Database.SetCommandTimeout(TimeSpan.FromSeconds(30)); + context.SaveChanges(); + + // clean up residuals + var looseFiles = context.Files.Where(f => f.Uploaded == false); + var unfinishedRegistrations = context.LodeStoneAuth.Where(c => c.StartedAt != null); + context.RemoveRange(unfinishedRegistrations); + context.RemoveRange(looseFiles); + 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.Permissions.AsNoTracking().Where(p=>p.IsPaused).Count()); + + } + + 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) + { + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.ClearProviders(); + builder.AddConsole(); + }); + var logger = loggerFactory.CreateLogger(); + return Host.CreateDefaultBuilder(args) + .UseSystemd() + .UseConsoleLifetime() + .ConfigureAppConfiguration((ctx, config) => + { + var appSettingsPath = Environment.GetEnvironmentVariable("APPSETTINGS_PATH"); + if (!string.IsNullOrEmpty(appSettingsPath)) + { + config.AddJsonFile(appSettingsPath, optional: true, reloadOnChange: true); + } + else + { + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); + } + + config.AddEnvironmentVariables(); + }) + .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/LightlessSyncServer/LightlessSyncServer/Properties/launchSettings.json b/LightlessSyncServer/LightlessSyncServer/Properties/launchSettings.json new file mode 100644 index 0000000..c782d45 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "profiles": { + "LightlessSyncServer": { + "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/LightlessSyncServer/LightlessSyncServer/Properties/serviceDependencies.json b/LightlessSyncServer/LightlessSyncServer/Properties/serviceDependencies.json new file mode 100644 index 0000000..33703d5 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Properties/serviceDependencies.json @@ -0,0 +1,3 @@ +{ + "dependencies": {} +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncServer/Properties/serviceDependencies.local.json b/LightlessSyncServer/LightlessSyncServer/Properties/serviceDependencies.local.json new file mode 100644 index 0000000..33703d5 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Properties/serviceDependencies.local.json @@ -0,0 +1,3 @@ +{ + "dependencies": {} +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncServer/Services/CharaDataCleanupService.cs b/LightlessSyncServer/LightlessSyncServer/Services/CharaDataCleanupService.cs new file mode 100644 index 0000000..a76eff5 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Services/CharaDataCleanupService.cs @@ -0,0 +1,42 @@ +using LightlessSyncShared.Data; +using Microsoft.EntityFrameworkCore; + +namespace LightlessSyncServer.Services; + +public class CharaDataCleanupService : BackgroundService +{ + private readonly ILogger _logger; + private readonly IDbContextFactory _dbContextFactory; + + public CharaDataCleanupService(ILogger logger, IDbContextFactory dbContextFactory) + { + _logger = logger; + _dbContextFactory = dbContextFactory; + } + + public override async Task StartAsync(CancellationToken cancellationToken) + { + await base.StartAsync(cancellationToken).ConfigureAwait(false); + _logger.LogInformation("Chara Data Cleanup Service started"); + } + + protected override async Task ExecuteAsync(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); + } + } +} diff --git a/LightlessSyncServer/LightlessSyncServer/Services/ClientPairPermissionsCleanupService.cs b/LightlessSyncServer/LightlessSyncServer/Services/ClientPairPermissionsCleanupService.cs new file mode 100644 index 0000000..e5c5947 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Services/ClientPairPermissionsCleanupService.cs @@ -0,0 +1,190 @@ + +using LightlessSyncShared.Data; +using LightlessSyncShared.Models; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils.Configuration; +using Microsoft.EntityFrameworkCore; +using System.Collections.Concurrent; +using System.Data; +using System.Diagnostics; + +namespace LightlessSyncServer.Services; + +public class ClientPairPermissionsCleanupService(ILogger _logger, IDbContextFactory _dbContextFactory, + IConfigurationService _configurationService) + : BackgroundService +{ + public override async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Client Pair Permissions Cleanup Service started"); + await base.StartAsync(cancellationToken).ConfigureAwait(false); + } + + private async Task AllUsersPermissionsCleanup(CancellationToken ct) + { + const int MaxParallelism = 8; + const int MaxProcessingPerChunk = 1000000; + + long removedEntries = 0; + long priorRemovedEntries = 0; + ConcurrentDictionary> toRemovePermsParallel = []; + ConcurrentDictionary completionDebugPrint = []; + int parallelProcessed = 0; + int userNo = 0; + int lastUserNo = 0; + + using var db = await _dbContextFactory.CreateDbContextAsync(ct).ConfigureAwait(false); + _logger.LogInformation("Building All Pairs"); + + _logger.LogInformation("Collecting Users"); + var users = (await db.Users.Select(k => k.UID).AsNoTracking().ToListAsync(ct).ConfigureAwait(false)).Order(StringComparer.Ordinal).ToList(); + + Stopwatch st = Stopwatch.StartNew(); + + while (userNo < users.Count) + { + using CancellationTokenSource loopCts = new(); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(loopCts.Token, ct); + try + { + await Parallel.ForAsync(userNo, users.Count, new ParallelOptions() + { + MaxDegreeOfParallelism = MaxParallelism, + CancellationToken = linkedCts.Token + }, + async (i, token) => + { + var userNoInc = Interlocked.Increment(ref userNo); + using var db2 = await _dbContextFactory.CreateDbContextAsync(token).ConfigureAwait(false); + + var user = users[i]; + var personalPairs = await GetAllPairsForUser(user, db2, ct).ConfigureAwait(false); + + toRemovePermsParallel[i] = await UserPermissionCleanup(i, users.Count, user, db2, personalPairs).ConfigureAwait(false); + var processedAdd = Interlocked.Add(ref parallelProcessed, toRemovePermsParallel[i].Count); + + var completionPcnt = userNoInc / (double)users.Count; + var completionInt = (int)(completionPcnt * 100); + + if (completionInt > 0 && (!completionDebugPrint.TryGetValue(completionInt, out bool posted) || !posted)) + { + completionDebugPrint[completionInt] = true; + var elapsed = st.Elapsed; + var estimatedTimeLeft = (elapsed / completionPcnt) - elapsed; + _logger.LogInformation("Progress: {no}/{total} ({pct:P2}), removed so far: {removed}, planned next chunk: {planned}, estimated time left: {time}", + userNoInc, users.Count, completionPcnt, removedEntries, processedAdd, estimatedTimeLeft); + if (userNoInc / (double)users.Count - lastUserNo / (double)users.Count > 0.05) + { + // 5% processed without writing, might as well save at this point + await loopCts.CancelAsync().ConfigureAwait(false); + } + } + + if (processedAdd > MaxProcessingPerChunk) + await loopCts.CancelAsync().ConfigureAwait(false); + }).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // expected + } + + removedEntries += parallelProcessed; + + try + { + parallelProcessed = 0; + + _logger.LogInformation("Removing {newDeleted} entities and writing to database", removedEntries - priorRemovedEntries); + db.Permissions.RemoveRange(toRemovePermsParallel.Values.SelectMany(v => v).ToList()); + await db.SaveChangesAsync(ct).ConfigureAwait(false); + + _logger.LogInformation("Removed {newDeleted} entities, settling...", removedEntries - priorRemovedEntries); + priorRemovedEntries = removedEntries; + lastUserNo = userNo; + } + catch (DBConcurrencyException ex) + { + _logger.LogWarning(ex, "Concurrency Exception during User Permissions Cleanup, restarting at {last}", lastUserNo); + userNo = lastUserNo; + removedEntries = priorRemovedEntries; + continue; + } + finally + { + toRemovePermsParallel.Clear(); + } + } + + st.Stop(); + _logger.LogInformation("User Permissions Cleanup Finished, removed {total} stale permissions in {time}", removedEntries, st.Elapsed); + } + + private async Task> UserPermissionCleanup(int userNr, int totalUsers, string uid, LightlessDbContext dbContext, List pairs) + { + var perms = dbContext.Permissions.Where(p => p.UserUID == uid && !p.Sticky && !pairs.Contains(p.OtherUserUID)); + + var permsToRemoveCount = await perms.CountAsync().ConfigureAwait(false); + if (permsToRemoveCount == 0) + return []; + + _logger.LogInformation("[{current}/{totalCount}] User {user}: Planning to remove {removed} permissions", userNr, totalUsers, uid, permsToRemoveCount); + + return await perms.ToListAsync().ConfigureAwait(false); + } + + private async Task> GetAllPairsForUser(string uid, LightlessDbContext dbContext, CancellationToken ct) + { + var entries = await dbContext.ClientPairs.AsNoTracking().Where(k => k.UserUID == uid).Select(k => k.OtherUserUID) + .Concat( + dbContext.GroupPairs.Where(k => k.GroupUserUID == uid).AsNoTracking() + .Join(dbContext.GroupPairs.AsNoTracking(), + a => a.GroupGID, + b => b.GroupGID, + (a, b) => b.GroupUserUID) + .Where(a => a != uid)) + .ToListAsync(ct).ConfigureAwait(false); + + return entries.Distinct(StringComparer.Ordinal).ToList(); + } + + protected override async Task ExecuteAsync(CancellationToken ct) + { + if (!_configurationService.GetValueOrDefault(nameof(ServerConfiguration.RunPermissionCleanupOnStartup), defaultValue: true)) + { + await WaitUntilNextCleanup(ct).ConfigureAwait(false); + } + + while (!ct.IsCancellationRequested) + { + try + { + _logger.LogInformation("Starting Permissions Cleanup"); + await AllUsersPermissionsCleanup(ct).ConfigureAwait(false); + } + catch (DbUpdateConcurrencyException ex) + { + _logger.LogWarning(ex, "Concurrency Exception during User Permissions Cleanup"); + continue; + } + catch (Exception ex) + { + _logger.LogError(ex, "Unhandled Exception during User Permissions Cleanup"); + } + + await WaitUntilNextCleanup(ct).ConfigureAwait(false); + } + } + + private async Task WaitUntilNextCleanup(CancellationToken token) + { + var now = DateTime.UtcNow; + var nextRun = new DateTime(now.Year, now.Month, now.Day, 12, 0, 0, DateTimeKind.Utc); + if (now > nextRun) nextRun = nextRun.AddDays(1); + + var nextRunSpan = nextRun - now; + _logger.LogInformation("Permissions Cleanup next run in {span}", nextRunSpan); + + await Task.Delay(nextRunSpan, token).ConfigureAwait(false); + } +} diff --git a/LightlessSyncServer/LightlessSyncServer/Services/GPoseLobbyDistributionService.cs b/LightlessSyncServer/LightlessSyncServer/Services/GPoseLobbyDistributionService.cs new file mode 100644 index 0000000..cb3693a --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Services/GPoseLobbyDistributionService.cs @@ -0,0 +1,226 @@ +using LightlessSync.API.Dto.CharaData; +using LightlessSync.API.SignalR; +using LightlessSyncServer.Hubs; +using Microsoft.AspNetCore.SignalR; +using StackExchange.Redis.Extensions.Core.Abstractions; + +namespace LightlessSyncServer.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(10), 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/LightlessSyncServer/LightlessSyncServer/Services/MareCensus.cs b/LightlessSyncServer/LightlessSyncServer/Services/MareCensus.cs new file mode 100644 index 0000000..decd202 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Services/MareCensus.cs @@ -0,0 +1,182 @@ +using LightlessSync.API.Dto.User; +using Microsoft.VisualBasic.FileIO; +using Prometheus; +using System.Collections.Concurrent; +using System.Globalization; + +namespace LightlessSyncServer.Services; + +public class LightlessCensus : IHostedService +{ + private record CensusEntry(ushort WorldId, short Race, short Subrace, short Gender) + { + public static CensusEntry FromDto(CensusDataDto dto) + { + return new CensusEntry(dto.WorldId, dto.RaceId, dto.TribeId, dto.Gender); + } + } + + private readonly ConcurrentDictionary _censusEntries = new(StringComparer.Ordinal); + private readonly Dictionary _dcs = new(); + private readonly Dictionary _gender = new(); + private readonly ILogger _logger; + private readonly Dictionary _races = new(); + private readonly Dictionary _tribes = new(); + private readonly Dictionary _worlds = new(); + private Gauge? _gauge; + + public LightlessCensus(ILogger logger) + { + _logger = logger; + } + + private bool Initialized => _gauge != null; + + public void ClearStatistics(string uid) + { + if (!Initialized) return; + + if (_censusEntries.Remove(uid, out var censusEntry)) + { + ModifyGauge(censusEntry, increase: false); + } + } + + public void PublishStatistics(string uid, CensusDataDto? censusDataDto) + { + if (!Initialized || censusDataDto == null) return; + + var newEntry = CensusEntry.FromDto(censusDataDto); + + if (_censusEntries.TryGetValue(uid, out var entry)) + { + if (entry != newEntry) + { + ModifyGauge(entry, increase: false); + ModifyGauge(newEntry, increase: true); + _censusEntries[uid] = newEntry; + } + } + else + { + _censusEntries[uid] = newEntry; + ModifyGauge(newEntry, increase: true); + } + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Loading XIVAPI data"); + + using HttpClient client = new HttpClient(); + + Dictionary worldDcs = new(); + + var dcs = await client.GetStringAsync("https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/WorldDCGroupType.csv", cancellationToken).ConfigureAwait(false); + // dc: https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/WorldDCGroupType.csv + // id, name, region + + using var dcsReader = new StringReader(dcs); + using var dcsParser = new TextFieldParser(dcsReader); + dcsParser.Delimiters = [","]; + // read 3 lines and discard + dcsParser.ReadLine(); dcsParser.ReadLine(); dcsParser.ReadLine(); + + while (!dcsParser.EndOfData) + { + var fields = dcsParser.ReadFields(); + var id = short.Parse(fields[0], CultureInfo.InvariantCulture); + var name = fields[1]; + if (string.IsNullOrEmpty(name) || id == 0) continue; + _logger.LogInformation("DC: ID: {id}, Name: {name}", id, name); + _dcs[id] = name; + } + + var worlds = await client.GetStringAsync("https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/World.csv", cancellationToken).ConfigureAwait(false); + // world: https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/World.csv + // id, internalname, name, region, usertype, datacenter, ispublic + + using var worldsReader = new StringReader(worlds); + using var worldsParser = new TextFieldParser(worldsReader); + worldsParser.Delimiters = [","]; + // read 3 lines and discard + worldsParser.ReadLine(); worldsParser.ReadLine(); worldsParser.ReadLine(); + + while (!worldsParser.EndOfData) + { + var fields = worldsParser.ReadFields(); + var id = ushort.Parse(fields[0], CultureInfo.InvariantCulture); + var name = fields[1]; + var dc = short.Parse(fields[5], CultureInfo.InvariantCulture); + var isPublic = bool.Parse(fields[6]); + if (!_dcs.ContainsKey(dc) || !isPublic) continue; + _worlds[id] = (name, dc); + _logger.LogInformation("World: ID: {id}, Name: {name}, DC: {dc}", id, name, dc); + } + + var races = await client.GetStringAsync("https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/Race.csv", cancellationToken).ConfigureAwait(false); + // race: https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/Race.csv + // id, masc name, fem name, other crap I don't care about + + using var raceReader = new StringReader(races); + using var raceParser = new TextFieldParser(raceReader); + raceParser.Delimiters = [","]; + // read 3 lines and discard + raceParser.ReadLine(); raceParser.ReadLine(); raceParser.ReadLine(); + + while (!raceParser.EndOfData) + { + var fields = raceParser.ReadFields(); + var id = short.Parse(fields[0], CultureInfo.InvariantCulture); + var name = fields[1]; + if (string.IsNullOrEmpty(name) || id == 0) continue; + _races[id] = name; + _logger.LogInformation("Race: ID: {id}, Name: {name}", id, name); + } + + var tribe = await client.GetStringAsync("https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/Tribe.csv", cancellationToken).ConfigureAwait(false); + // tribe: https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/Tribe.csv + // id masc name, fem name, other crap I don't care about + + using var tribeReader = new StringReader(tribe); + using var tribeParser = new TextFieldParser(tribeReader); + tribeParser.Delimiters = [","]; + // read 3 lines and discard + tribeParser.ReadLine(); tribeParser.ReadLine(); tribeParser.ReadLine(); + + while (!tribeParser.EndOfData) + { + var fields = tribeParser.ReadFields(); + var id = short.Parse(fields[0], CultureInfo.InvariantCulture); + var name = fields[1]; + if (string.IsNullOrEmpty(name) || id == 0) continue; + _tribes[id] = name; + _logger.LogInformation("Tribe: ID: {id}, Name: {name}", id, name); + } + + _gender[0] = "Male"; + _gender[1] = "Female"; + + _gauge = Metrics.CreateGauge("lightless_census", "lightless informational census data", new[] { "dc", "world", "gender", "race", "subrace" }); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + private void ModifyGauge(CensusEntry censusEntry, bool increase) + { + var subraceSuccess = _tribes.TryGetValue(censusEntry.Subrace, out var subrace); + var raceSuccess = _races.TryGetValue(censusEntry.Race, out var race); + var worldSuccess = _worlds.TryGetValue(censusEntry.WorldId, out var world); + var genderSuccess = _gender.TryGetValue(censusEntry.Gender, out var gender); + if (subraceSuccess && raceSuccess && worldSuccess && genderSuccess && _dcs.TryGetValue(world.Item2, out var dc)) + { + if (increase) + _gauge.WithLabels(dc, world.Item1, gender, race, subrace).Inc(); + else + _gauge.WithLabels(dc, world.Item1, gender, race, subrace).Dec(); + } + } +} diff --git a/LightlessSyncServer/LightlessSyncServer/Services/OnlineSyncedPairCacheService.cs b/LightlessSyncServer/LightlessSyncServer/Services/OnlineSyncedPairCacheService.cs new file mode 100644 index 0000000..9a98d63 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Services/OnlineSyncedPairCacheService.cs @@ -0,0 +1,145 @@ +using LightlessSyncShared.Metrics; + +namespace LightlessSyncServer.Services; + +public class OnlineSyncedPairCacheService +{ + private readonly Dictionary _lastSeenCache = new(StringComparer.Ordinal); + private readonly SemaphoreSlim _cacheModificationSemaphore = new(1); + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private readonly LightlessMetrics _lightlessMetrics; + + public OnlineSyncedPairCacheService(ILogger logger, ILoggerFactory loggerFactory, LightlessMetrics lightlessMetrics) + { + _logger = logger; + _loggerFactory = loggerFactory; + _lightlessMetrics = lightlessMetrics; + } + + public async Task InitPlayer(string user) + { + if (_lastSeenCache.ContainsKey(user)) return; + + await _cacheModificationSemaphore.WaitAsync().ConfigureAwait(false); + try + { + _logger.LogDebug("Initializing {user}", user); + _lastSeenCache[user] = new(_loggerFactory.CreateLogger(), user, _lightlessMetrics); + } + finally + { + _cacheModificationSemaphore.Release(); + } + } + + public async Task DisposePlayer(string user) + { + if (!_lastSeenCache.ContainsKey(user)) return; + + await _cacheModificationSemaphore.WaitAsync().ConfigureAwait(false); + try + { + _logger.LogDebug("Disposing {user}", user); + _lastSeenCache.Remove(user, out var pairCache); + pairCache?.Dispose(); + } + finally + { + _cacheModificationSemaphore.Release(); + } + } + + public async Task AreAllPlayersCached(string sender, List uids, CancellationToken ct) + { + if (!_lastSeenCache.ContainsKey(sender)) await InitPlayer(sender).ConfigureAwait(false); + + _lastSeenCache.TryGetValue(sender, out var pairCache); + return await pairCache.AreAllPlayersCached(uids, ct).ConfigureAwait(false); + } + + public async Task CachePlayers(string sender, List uids, CancellationToken ct) + { + if (!_lastSeenCache.ContainsKey(sender)) await InitPlayer(sender).ConfigureAwait(false); + + _lastSeenCache.TryGetValue(sender, out var pairCache); + await pairCache.CachePlayers(uids, ct).ConfigureAwait(false); + } + + private sealed class PairCache : IDisposable + { + private readonly ILogger _logger; + private readonly string _owner; + private readonly LightlessMetrics _metrics; + private readonly Dictionary _lastSeenCache = new(StringComparer.Ordinal); + private readonly SemaphoreSlim _lock = new(1); + + public PairCache(ILogger logger, string owner, LightlessMetrics metrics) + { + metrics.IncGauge(MetricsAPI.GaugeUserPairCacheUsers); + _logger = logger; + _owner = owner; + _metrics = metrics; + } + + public async Task AreAllPlayersCached(List uids, CancellationToken ct) + { + await _lock.WaitAsync(ct).ConfigureAwait(false); + + try + { + var allCached = uids.TrueForAll(u => _lastSeenCache.TryGetValue(u, out var expiry) && expiry > DateTime.UtcNow); + + _logger.LogDebug("AreAllPlayersCached:{uid}:{count}:{result}", _owner, uids.Count, allCached); + + if (allCached) _metrics.IncCounter(MetricsAPI.CounterUserPairCacheHit); + else _metrics.IncCounter(MetricsAPI.CounterUserPairCacheMiss); + + return allCached; + } + finally + { + _lock.Release(); + } + } + + public async Task CachePlayers(List uids, CancellationToken ct) + { + await _lock.WaitAsync(ct).ConfigureAwait(false); + try + { + var lastSeen = DateTime.UtcNow.AddMinutes(60); + _logger.LogDebug("CacheOnlinePlayers:{uid}:{count}", _owner, uids.Count); + var newEntries = uids.Count(u => !_lastSeenCache.ContainsKey(u)); + + _metrics.IncCounter(MetricsAPI.CounterUserPairCacheNewEntries, newEntries); + _metrics.IncCounter(MetricsAPI.CounterUserPairCacheUpdatedEntries, uids.Count - newEntries); + + _metrics.IncGauge(MetricsAPI.GaugeUserPairCacheEntries, newEntries); + uids.ForEach(u => _lastSeenCache[u] = lastSeen); + + // clean up old entries + var outdatedEntries = _lastSeenCache.Where(u => u.Value < DateTime.UtcNow).Select(k => k.Key).ToList(); + if (outdatedEntries.Any()) + { + _metrics.DecGauge(MetricsAPI.GaugeUserPairCacheEntries, outdatedEntries.Count); + foreach (var entry in outdatedEntries) + { + _lastSeenCache.Remove(entry); + } + } + } + finally + { + _lock.Release(); + } + } + + public void Dispose() + { + _metrics.DecGauge(MetricsAPI.GaugeUserPairCacheUsers); + _metrics.DecGauge(MetricsAPI.GaugeUserPairCacheEntries, _lastSeenCache.Count); + _lock.Dispose(); + } + } +} diff --git a/LightlessSyncServer/LightlessSyncServer/Services/SystemInfoService.cs b/LightlessSyncServer/LightlessSyncServer/Services/SystemInfoService.cs new file mode 100644 index 0000000..449510a --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Services/SystemInfoService.cs @@ -0,0 +1,84 @@ +using LightlessSync.API.Dto; +using LightlessSync.API.SignalR; +using LightlessSyncServer.Hubs; +using LightlessSyncShared.Data; +using LightlessSyncShared.Metrics; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils.Configuration; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using StackExchange.Redis.Extensions.Core.Abstractions; + +namespace LightlessSyncServer.Services; + +public sealed class SystemInfoService : BackgroundService +{ + private readonly LightlessMetrics _lightlessMetrics; + private readonly IConfigurationService _config; + private readonly IDbContextFactory _dbContextFactory; + private readonly ILogger _logger; + private readonly IHubContext _hubContext; + private readonly IRedisDatabase _redis; + public SystemInfoDto SystemInfoDto { get; private set; } = new(); + + public SystemInfoService(LightlessMetrics lightlessMetrics, IConfigurationService configurationService, IDbContextFactory dbContextFactory, + ILogger logger, IHubContext hubContext, IRedisDatabase redisDb) + { + _lightlessMetrics = lightlessMetrics; + _config = configurationService; + _dbContextFactory = dbContextFactory; + _logger = logger; + _hubContext = hubContext; + _redis = redisDb; + } + + public override async Task StartAsync(CancellationToken cancellationToken) + { + await base.StartAsync(cancellationToken).ConfigureAwait(false); + _logger.LogInformation("System Info Service started"); + } + + protected override async Task ExecuteAsync(CancellationToken ct) + { + var timeOut = _config.IsMain ? 15 : 30; + + while (!ct.IsCancellationRequested) + { + try + { + ThreadPool.GetAvailableThreads(out int workerThreads, out int ioThreads); + + _lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeAvailableWorkerThreads, workerThreads); + _lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeAvailableIOWorkerThreads, ioThreads); + + var onlineUsers = (_redis.SearchKeysAsync("UID:*").GetAwaiter().GetResult()).Count(); + SystemInfoDto = new SystemInfoDto() + { + OnlineUsers = onlineUsers, + }; + + if (_config.IsMain) + { + _logger.LogInformation("Sending System Info, Online Users: {onlineUsers}", onlineUsers); + + await _hubContext.Clients.All.Client_UpdateSystemInfo(SystemInfoDto).ConfigureAwait(false); + + using var db = await _dbContextFactory.CreateDbContextAsync(ct).ConfigureAwait(false); + + _lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeAuthorizedConnections, onlineUsers); + _lightlessMetrics.SetGaugeTo(MetricsAPI.GaugePairs, db.ClientPairs.AsNoTracking().Count()); + _lightlessMetrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, db.Permissions.AsNoTracking().Where(p => p.IsPaused).Count()); + _lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeGroups, db.Groups.AsNoTracking().Count()); + _lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeGroupPairs, db.GroupPairs.AsNoTracking().Count()); + _lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeUsersRegistered, db.Users.AsNoTracking().Count()); + } + + await Task.Delay(TimeSpan.FromSeconds(timeOut), ct).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to push system info"); + } + } + } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncServer/Services/UserCleanupService.cs b/LightlessSyncServer/LightlessSyncServer/Services/UserCleanupService.cs new file mode 100644 index 0000000..76f330c --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Services/UserCleanupService.cs @@ -0,0 +1,194 @@ +using LightlessSyncShared.Data; +using LightlessSyncShared.Metrics; +using LightlessSyncShared.Models; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils; +using LightlessSyncShared.Utils.Configuration; +using Microsoft.EntityFrameworkCore; + +namespace LightlessSyncServer.Services; + +public class UserCleanupService : IHostedService +{ + private readonly LightlessMetrics metrics; + private readonly ILogger _logger; + private readonly IDbContextFactory _lightlessDbContextFactory; + private readonly IConfigurationService _configuration; + private CancellationTokenSource _cleanupCts; + + public UserCleanupService(LightlessMetrics metrics, ILogger logger, IDbContextFactory lightlessDbContextFactory, IConfigurationService configuration) + { + this.metrics = metrics; + _logger = logger; + _lightlessDbContextFactory = lightlessDbContextFactory; + _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 _lightlessDbContextFactory.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(LightlessDbContext 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(LightlessDbContext 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); + } + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error during user purge"); + } + } + + private void CleanUpOutdatedLodestoneAuths(LightlessDbContext 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, LightlessDbContext 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 userFiles = dbContext.Files.Where(f => f.Uploaded && f.Uploader.UID == user.UID).ToList(); + dbContext.Files.RemoveRange(userFiles); + + 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); + } + + _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/LightlessSyncServer/LightlessSyncServer/Startup.cs b/LightlessSyncServer/LightlessSyncServer/Startup.cs new file mode 100644 index 0000000..4a2a2ba --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Startup.cs @@ -0,0 +1,367 @@ +using Microsoft.EntityFrameworkCore; +using LightlessSyncServer.Hubs; +using Microsoft.AspNetCore.Http.Connections; +using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.Authorization; +using AspNetCoreRateLimit; +using LightlessSyncShared.Data; +using LightlessSyncShared.Metrics; +using LightlessSyncServer.Services; +using LightlessSyncShared.Utils; +using LightlessSyncShared.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 LightlessSync.API.SignalR; +using MessagePack; +using MessagePack.Resolvers; +using Microsoft.AspNetCore.Mvc.Controllers; +using LightlessSyncServer.Controllers; +using LightlessSyncShared.RequirementHandlers; +using LightlessSyncShared.Utils.Configuration; + +namespace LightlessSyncServer; + +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 lightlessConfig = Configuration.GetRequiredSection("LightlessSync"); + + // configure metrics + ConfigureMetrics(services); + + // configure database + ConfigureDatabase(services, lightlessConfig); + + // configure authentication and authorization + ConfigureAuthorization(services); + + // configure rate limiting + ConfigureIpRateLimiting(services); + + // configure SignalR + ConfigureSignalR(services, lightlessConfig); + + // configure lightless specific services + ConfigureLightlessServices(services, lightlessConfig); + + services.AddHealthChecks(); + services.AddControllers().ConfigureApplicationPartManager(a => + { + a.FeatureProviders.Remove(a.FeatureProviders.OfType().First()); + if (lightlessConfig.GetValue(nameof(ServerConfiguration.MainServerAddress), defaultValue: null) == null) + { + a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(LightlessServerConfigurationController), typeof(LightlessBaseConfigurationController), typeof(ClientMessageController))); + } + else + { + a.FeatureProviders.Add(new AllowedControllersFeatureProvider()); + } + }); + } + + private void ConfigureLightlessServices(IServiceCollection services, IConfigurationSection lightlessConfig) + { + bool isMainServer = lightlessConfig.GetValue(nameof(ServerConfiguration.MainServerAddress), defaultValue: null) == null; + + services.Configure(Configuration.GetRequiredSection("LightlessSync")); + services.Configure(Configuration.GetRequiredSection("LightlessSync")); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(provider => provider.GetService()); + // configure services based on main server status + ConfigureServicesBasedOnShardType(services, lightlessConfig, isMainServer); + + services.AddSingleton(s => new LightlessCensus(s.GetRequiredService>())); + services.AddHostedService(p => p.GetRequiredService()); + + if (isMainServer) + { + services.AddSingleton(); + services.AddHostedService(provider => provider.GetService()); + services.AddSingleton(); + services.AddHostedService(provider => provider.GetService()); + services.AddHostedService(); + } + + services.AddSingleton(); + services.AddHostedService(provider => provider.GetService()); + } + + private static void ConfigureSignalR(IServiceCollection services, IConfigurationSection lightlessConfig) + { + services.AddSingleton(); + services.AddSingleton(); + + var signalRServiceBuilder = services.AddSignalR(hubOptions => + { + hubOptions.MaximumReceiveMessageSize = long.MaxValue; + hubOptions.EnableDetailedErrors = true; + hubOptions.MaximumParallelInvocationsPerClient = 10; + hubOptions.StreamBufferCapacity = 200; + + hubOptions.AddFilter(); + 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 = lightlessConfig.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 = lightlessConfig.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.AddTransient(); + 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(LightlessConfigurationBase.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(); + policy.AddRequirements(new ValidTokenRequirement()); + }); + options.AddPolicy("Identified", policy => + { + policy.AddRequirements(new UserRequirement(UserRequirements.Identified)); + policy.AddRequirements(new ValidTokenRequirement()); + + }); + options.AddPolicy("Admin", policy => + { + policy.AddRequirements(new UserRequirement(UserRequirements.Identified | UserRequirements.Administrator)); + policy.AddRequirements(new ValidTokenRequirement()); + + }); + options.AddPolicy("Moderator", policy => + { + policy.AddRequirements(new UserRequirement(UserRequirements.Identified | UserRequirements.Moderator | UserRequirements.Administrator)); + policy.AddRequirements(new ValidTokenRequirement()); + }); + options.AddPolicy("Internal", new AuthorizationPolicyBuilder().RequireClaim(LightlessClaimTypes.Internal, "true").Build()); + }); + } + + private void ConfigureDatabase(IServiceCollection services, IConfigurationSection lightlessConfig) + { + services.AddDbContextPool(options => + { + options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder => + { + builder.MigrationsHistoryTable("_efmigrationshistory", "public"); + builder.MigrationsAssembly("LightlessSyncShared"); + }).UseSnakeCaseNamingConvention(); + options.EnableThreadSafetyChecks(false); + }, lightlessConfig.GetValue(nameof(LightlessConfigurationBase.DbContextPoolSize), 1024)); + services.AddDbContextFactory(options => + { + options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder => + { + builder.MigrationsHistoryTable("_efmigrationshistory", "public"); + builder.MigrationsAssembly("LightlessSyncShared"); + }).UseSnakeCaseNamingConvention(); + options.EnableThreadSafetyChecks(false); + }); + } + + private static void ConfigureMetrics(IServiceCollection services) + { + services.AddSingleton(m => new LightlessMetrics(m.GetService>(), new List + { + MetricsAPI.CounterInitializedConnections, + MetricsAPI.CounterUserPushData, + MetricsAPI.CounterUserPushDataTo, + MetricsAPI.CounterUsersRegisteredDeleted, + MetricsAPI.CounterAuthenticationCacheHits, + MetricsAPI.CounterAuthenticationFailures, + MetricsAPI.CounterAuthenticationRequests, + MetricsAPI.CounterAuthenticationSuccesses, + MetricsAPI.CounterUserPairCacheHit, + MetricsAPI.CounterUserPairCacheMiss, + MetricsAPI.CounterUserPairCacheNewEntries, + MetricsAPI.CounterUserPairCacheUpdatedEntries, + }, new List + { + MetricsAPI.GaugeAuthorizedConnections, + MetricsAPI.GaugeConnections, + MetricsAPI.GaugePairs, + MetricsAPI.GaugePairsPaused, + MetricsAPI.GaugeAvailableIOWorkerThreads, + MetricsAPI.GaugeAvailableWorkerThreads, + MetricsAPI.GaugeGroups, + MetricsAPI.GaugeGroupPairs, + MetricsAPI.GaugeUsersRegistered, + MetricsAPI.GaugeAuthenticationCacheEntries, + MetricsAPI.GaugeUserPairCacheEntries, + MetricsAPI.GaugeUserPairCacheUsers, + MetricsAPI.GaugeGposeLobbies, + MetricsAPI.GaugeGposeLobbyUsers, + MetricsAPI.GaugeHubConcurrency, + MetricsAPI.GaugeHubQueuedConcurrency, + })); + } + + private static void ConfigureServicesBasedOnShardType(IServiceCollection services, IConfigurationSection lightlessConfig, bool isMainServer) + { + if (!isMainServer) + { + services.AddSingleton, LightlessConfigurationServiceClient>(); + services.AddSingleton, LightlessConfigurationServiceClient>(); + + services.AddHostedService(p => (LightlessConfigurationServiceClient)p.GetService>()); + services.AddHostedService(p => (LightlessConfigurationServiceClient)p.GetService>()); + } + else + { + services.AddSingleton, LightlessConfigurationServiceServer>(); + services.AddSingleton, LightlessConfigurationServiceServer>(); + } + } + + 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(LightlessConfigurationBase.MetricsPort), 4980)); + metricServer.Start(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapHub(ILightlessHub.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/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs b/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs new file mode 100644 index 0000000..070ff9e --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs @@ -0,0 +1,74 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Data.Enum; +using LightlessSync.API.Data.Extensions; +using LightlessSyncShared.Models; +using static LightlessSyncServer.Hubs.LightlessHub; + +namespace LightlessSyncServer.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 IndividualPairStatus ToIndividualPairStatus(this UserInfo userInfo) + { + if (userInfo.IndividuallyPaired) return IndividualPairStatus.Bidirectional; + if (!userInfo.IndividuallyPaired && userInfo.GIDs.Contains(Constants.IndividualKeyword, StringComparer.Ordinal)) return IndividualPairStatus.OneSided; + return IndividualPairStatus.None; + } + + public static GroupPermissions ToEnum(this Group group) + { + var permissions = GroupPermissions.NoneSet; + permissions.SetPreferDisableAnimations(group.PreferDisableAnimations); + permissions.SetPreferDisableSounds(group.PreferDisableSounds); + permissions.SetPreferDisableVFX(group.PreferDisableVFX); + permissions.SetDisableInvites(!group.InvitesEnabled); + return permissions; + } + + public static GroupUserPreferredPermissions ToEnum(this GroupPairPreferredPermission groupPair) + { + var permissions = GroupUserPreferredPermissions.NoneSet; + permissions.SetDisableAnimations(groupPair.DisableAnimations); + permissions.SetDisableSounds(groupPair.DisableSounds); + permissions.SetPaused(groupPair.IsPaused); + permissions.SetDisableVFX(groupPair.DisableVFX); + return permissions; + } + + public static GroupPairUserInfo ToEnum(this GroupPair groupPair) + { + var groupUserInfo = GroupPairUserInfo.None; + groupUserInfo.SetPinned(groupPair.IsPinned); + groupUserInfo.SetModerator(groupPair.IsModerator); + return groupUserInfo; + } + + public static UserPermissions ToUserPermissions(this UserPermissionSet? permissions, bool setSticky = false) + { + if (permissions == null) return UserPermissions.NoneSet; + + UserPermissions perm = UserPermissions.NoneSet; + perm.SetPaused(permissions.IsPaused); + perm.SetDisableAnimations(permissions.DisableAnimations); + perm.SetDisableSounds(permissions.DisableSounds); + perm.SetDisableVFX(permissions.DisableVFX); + if (setSticky) + perm.SetSticky(permissions.Sticky); + return perm; + } +} diff --git a/LightlessSyncServer/LightlessSyncServer/Utils/MareHubLogger.cs b/LightlessSyncServer/LightlessSyncServer/Utils/MareHubLogger.cs new file mode 100644 index 0000000..03d92d9 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Utils/MareHubLogger.cs @@ -0,0 +1,34 @@ +using LightlessSync.API.SignalR; +using LightlessSyncServer.Hubs; +using System.Runtime.CompilerServices; + +namespace LightlessSyncServer.Utils; + +public class LightlessHubLogger +{ + private readonly LightlessHub _hub; + private readonly ILogger _logger; + + public LightlessHubLogger(LightlessHub 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/LightlessSyncServer/LightlessSyncServer/Utils/PauseInfo.cs b/LightlessSyncServer/LightlessSyncServer/Utils/PauseInfo.cs new file mode 100644 index 0000000..630d61d --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Utils/PauseInfo.cs @@ -0,0 +1,8 @@ +namespace LightlessSyncServer.Utils; + +public enum PauseInfo +{ + NoConnection, + Paused, + Unpaused, +} diff --git a/LightlessSyncServer/LightlessSyncServer/Utils/PauseState.cs b/LightlessSyncServer/LightlessSyncServer/Utils/PauseState.cs new file mode 100644 index 0000000..960a785 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Utils/PauseState.cs @@ -0,0 +1,9 @@ +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncServer/Utils/PausedEntry.cs b/LightlessSyncServer/LightlessSyncServer/Utils/PausedEntry.cs new file mode 100644 index 0000000..c4771b8 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Utils/PausedEntry.cs @@ -0,0 +1,58 @@ +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncServer/Utils/UserPair.cs b/LightlessSyncServer/LightlessSyncServer/Utils/UserPair.cs new file mode 100644 index 0000000..5da1d2a --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Utils/UserPair.cs @@ -0,0 +1,12 @@ +namespace LightlessSyncServer.Hubs; + +public partial class LightlessHub +{ + 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/LightlessSyncServer/LightlessSyncServer/appsettings.Development.json b/LightlessSyncServer/LightlessSyncServer/appsettings.Development.json new file mode 100644 index 0000000..5173757 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/LightlessSyncServer/LightlessSyncServer/appsettings.json b/LightlessSyncServer/LightlessSyncServer/appsettings.json new file mode 100644 index 0000000..880bf73 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/appsettings.json @@ -0,0 +1,61 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5432;Database=;Username=;Password=" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "LightlessSyncServer.Authentication": "Warning", + "System.IO.IOException": "Warning" + }, + "File": { + "BasePath": "logs", + "FileAccessMode": "KeepOpenAndAutoFlush", + "FileEncodingName": "utf-8", + "DateFormat": "yyyMMdd", + "MaxFileSize": 10485760, + "Files": [ + { + "Path": "lightless-.log" + } + ] + } + }, + "LightlessSync": { + "DbContextPoolSize": 2000, + "CdnFullUrl": "http://localhost/cache/", + "ServiceAddress": "http://localhost:5002", + "StaticFileServiceAddress": "http://localhost:5003" + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://+:6000", + "Certificate": { + "Subject": "sync.lightless-sync.org", + "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/LightlessSyncServer/LightlessSyncServerTest/Discord/DiscordBotTest.cs b/LightlessSyncServer/LightlessSyncServerTest/Discord/DiscordBotTest.cs new file mode 100644 index 0000000..d57ca55 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServerTest/Discord/DiscordBotTest.cs @@ -0,0 +1,53 @@ +using FluentAssertions; +using LightlessSyncServer.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 LightlessSyncServerTest.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/LightlessSyncServer/LightlessSyncServerTest/Hubs/MareHubTest.cs b/LightlessSyncServer/LightlessSyncServerTest/Hubs/MareHubTest.cs new file mode 100644 index 0000000..c3c0dbf --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServerTest/Hubs/MareHubTest.cs @@ -0,0 +1,82 @@ +using LightlessSyncServer.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 LightlessSyncShared.Data; +using LightlessSyncShared.Models; + +namespace LightlessSyncServerTest.Hubs { + public class LightlessHubTest { + + [Test] + public async Task Disconnect_QueryReturnsCorrectResult_Test() { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: "lightless").Options; + + using var context = new LightlessDbContext(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 LightlessHub(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/LightlessSyncServer/LightlessSyncServerTest/LightlessSyncServerTest.csproj b/LightlessSyncServer/LightlessSyncServerTest/LightlessSyncServerTest.csproj new file mode 100644 index 0000000..471a7a3 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServerTest/LightlessSyncServerTest.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + + + + + diff --git a/LightlessSyncServer/LightlessSyncServerTest/Usings.cs b/LightlessSyncServer/LightlessSyncServerTest/Usings.cs new file mode 100644 index 0000000..cefced4 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServerTest/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncServices/Discord/DiscordBot.cs b/LightlessSyncServer/LightlessSyncServices/Discord/DiscordBot.cs new file mode 100644 index 0000000..9fd515f --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServices/Discord/DiscordBot.cs @@ -0,0 +1,434 @@ +using Discord; +using Discord.Interactions; +using Discord.Rest; +using Discord.WebSocket; +using LightlessSyncShared.Data; +using LightlessSyncShared.Models; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils.Configuration; +using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json.Linq; +using StackExchange.Redis; + +namespace LightlessSyncServices.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 IDbContextFactory _dbContextFactory; + private readonly IServiceProvider _services; + private InteractionService _interactionModule; + private readonly CancellationTokenSource? _processReportQueueCts; + private CancellationTokenSource? _clientConnectedCts; + + public DiscordBot(DiscordBotServices botServices, IServiceProvider services, IConfigurationService configuration, + IDbContextFactory dbContextFactory, + ILogger logger, IConnectionMultiplexer connectionMultiplexer) + { + _botServices = botServices; + _services = services; + _configurationService = configuration; + _dbContextFactory = dbContextFactory; + _logger = logger; + _connectionMultiplexer = connectionMultiplexer; + _discordClient = new(new DiscordSocketConfig() + { + DefaultRetryMode = RetryMode.AlwaysRetry, + GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.GuildMembers + }); + + _discordClient.Log += Log; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + var token = _configurationService.GetValueOrDefault(nameof(ServicesConfiguration.DiscordBotToken), string.Empty); + if (!string.IsNullOrEmpty(token)) + { + _logger.LogInformation("Starting DiscordBot"); + _logger.LogInformation("Using Configuration: " + _configurationService.ToString()); + + _interactionModule?.Dispose(); + _interactionModule = new InteractionService(_discordClient); + _interactionModule.Log += Log; + await _interactionModule.AddModuleAsync(typeof(LightlessModule), _services).ConfigureAwait(false); + await _interactionModule.AddModuleAsync(typeof(LightlessWizardModule), _services).ConfigureAwait(false); + + await _discordClient.LoginAsync(TokenType.Bot, token).ConfigureAwait(false); + await _discordClient.StartAsync().ConfigureAwait(false); + + _discordClient.Ready += DiscordClient_Ready; + _discordClient.InteractionCreated += async (x) => + { + var ctx = new SocketInteractionContext(_discordClient, x); + await _interactionModule.ExecuteCommandAsync(ctx, _services).ConfigureAwait(false); + }; + _discordClient.UserJoined += OnUserJoined; + + await _botServices.Start().ConfigureAwait(false); + } + } + + private async Task OnUserJoined(SocketGuildUser arg) + { + try + { + using LightlessDbContext dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false); + var alreadyRegistered = await dbContext.LodeStoneAuth.AnyAsync(u => u.DiscordId == arg.Id).ConfigureAwait(false); + if (alreadyRegistered) + { + await _botServices.AddRegisteredRoleAsync(arg).ConfigureAwait(false); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to set user role on join"); + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + if (!string.IsNullOrEmpty(_configurationService.GetValueOrDefault(nameof(ServicesConfiguration.DiscordBotToken), string.Empty))) + { + await _botServices.Stop().ConfigureAwait(false); + _processReportQueueCts?.Cancel(); + _clientConnectedCts?.Cancel(); + + await _discordClient.LogoutAsync().ConfigureAwait(false); + await _discordClient.StopAsync().ConfigureAwait(false); + _interactionModule?.Dispose(); + } + } + + private async Task DiscordClient_Ready() + { + var guild = (await _discordClient.Rest.GetGuildsAsync().ConfigureAwait(false)).First(); + await _interactionModule.RegisterCommandsToGuildAsync(guild.Id, true).ConfigureAwait(false); + _clientConnectedCts?.Cancel(); + _clientConnectedCts?.Dispose(); + _clientConnectedCts = new(); + _ = UpdateStatusAsync(_clientConnectedCts.Token); + + await CreateOrUpdateModal(guild).ConfigureAwait(false); + _botServices.UpdateGuild(guild); + await _botServices.LogToChannel("Bot startup complete.").ConfigureAwait(false); + _ = UpdateVanityRoles(guild, _clientConnectedCts.Token); + _ = RemoveUsersNotInVanityRole(_clientConnectedCts.Token); + _ = RemoveUnregisteredUsers(_clientConnectedCts.Token); + } + + private async Task UpdateVanityRoles(RestGuild guild, CancellationToken token) + { + while (!token.IsCancellationRequested) + { + try + { + _logger.LogInformation("Updating Vanity Roles"); + Dictionary vanityRoles = _configurationService.GetValueOrDefault(nameof(ServicesConfiguration.VanityRoles), new Dictionary()); + if (vanityRoles.Keys.Count != _botServices.VanityRoles.Count) + { + _botServices.VanityRoles.Clear(); + foreach (var role in vanityRoles) + { + _logger.LogInformation("Adding Role: {id} => {desc}", role.Key, role.Value); + + var restrole = guild.GetRole(role.Key); + if (restrole != null) + _botServices.VanityRoles[restrole] = role.Value; + } + } + + await Task.Delay(TimeSpan.FromSeconds(30), token).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error during UpdateVanityRoles"); + } + } + } + + private async Task CreateOrUpdateModal(RestGuild guild) + { + _logger.LogInformation("Creating Wizard: Getting Channel"); + + var discordChannelForCommands = _configurationService.GetValue(nameof(ServicesConfiguration.DiscordChannelForCommands)); + if (discordChannelForCommands == null) + { + _logger.LogWarning("Creating Wizard: No channel configured"); + return; + } + + IUserMessage? message = null; + var socketchannel = await _discordClient.GetChannelAsync(discordChannelForCommands.Value).ConfigureAwait(false) as SocketTextChannel; + var pinnedMessages = await socketchannel.GetPinnedMessagesAsync().ConfigureAwait(false); + foreach (var msg in pinnedMessages) + { + _logger.LogInformation("Creating Wizard: Checking message id {id}, author is: {author}, hasEmbeds: {embeds}", msg.Id, msg.Author.Id, msg.Embeds.Any()); + if (msg.Author.Id == _discordClient.CurrentUser.Id + && msg.Embeds.Any()) + { + message = await socketchannel.GetMessageAsync(msg.Id).ConfigureAwait(false) as IUserMessage; + break; + } + } + + _logger.LogInformation("Creating Wizard: Found message id: {id}", message?.Id ?? 0); + + await GenerateOrUpdateWizardMessage(socketchannel, message).ConfigureAwait(false); + } + + private async Task GenerateOrUpdateWizardMessage(SocketTextChannel channel, IUserMessage? prevMessage) + { + EmbedBuilder eb = new EmbedBuilder(); + eb.WithTitle("Lightless Services Bot Interaction Service"); + eb.WithDescription("Press \"Start\" to interact with this bot!" + Environment.NewLine + Environment.NewLine + + "You can handle all of your Lightless account needs in this server through the easy to use interactive bot prompt. Just follow the instructions!"); + eb.WithThumbnailUrl("https://raw.githubusercontent.com/Light-Public-Syncshells/LightlessSync/refs/heads/main/LightlessSync/icon.png"); + var cb = new ComponentBuilder(); + cb.WithButton("Start", style: ButtonStyle.Primary, customId: "wizard-captcha:true", emote: Emoji.Parse("➡️")); + if (prevMessage == null) + { + var msg = await channel.SendMessageAsync(embed: eb.Build(), components: cb.Build()).ConfigureAwait(false); + try + { + await msg.PinAsync().ConfigureAwait(false); + } + catch (Exception) + { + // swallow + } + } + else + { + await prevMessage.ModifyAsync(p => + { + p.Embed = eb.Build(); + p.Components = cb.Build(); + }).ConfigureAwait(false); + } + } + + private Task Log(LogMessage msg) + { + switch (msg.Severity) + { + case LogSeverity.Critical: + case LogSeverity.Error: + _logger.LogError(msg.Exception, msg.Message); break; + case LogSeverity.Warning: + _logger.LogWarning(msg.Exception, msg.Message); break; + default: + _logger.LogInformation(msg.Message); break; + } + + return Task.CompletedTask; + } + + private async Task RemoveUnregisteredUsers(CancellationToken token) + { + var guild = (await _discordClient.Rest.GetGuildsAsync().ConfigureAwait(false)).First(); + while (!token.IsCancellationRequested) + { + try + { + await ProcessUserRoles(guild, token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // do nothing + } + catch (Exception ex) + { + await _botServices.LogToChannel($"Error during user procesing: {ex.Message}").ConfigureAwait(false); + } + + await Task.Delay(TimeSpan.FromDays(1)).ConfigureAwait(false); + } + } + + private async Task ProcessUserRoles(RestGuild guild, CancellationToken token) + { + using LightlessDbContext dbContext = await _dbContextFactory.CreateDbContextAsync(token).ConfigureAwait(false); + var roleId = _configurationService.GetValueOrDefault(nameof(ServicesConfiguration.DiscordRoleRegistered), 0); + var kickUnregistered = _configurationService.GetValueOrDefault(nameof(ServicesConfiguration.KickNonRegisteredUsers), false); + if (roleId == null) return; + + var registrationRole = guild.Roles.FirstOrDefault(f => f.Id == roleId.Value); + var registeredUsers = new HashSet(await dbContext.LodeStoneAuth.AsNoTracking().Select(c => c.DiscordId).ToListAsync().ConfigureAwait(false)); + + var executionStartTime = DateTimeOffset.UtcNow; + + int processedUsers = 0; + int addedRoles = 0; + int kickedUsers = 0; + int totalRoles = 0; + int toRemoveUsers = 0; + int freshUsers = 0; + + await _botServices.LogToChannel($"Starting to process registered users: Adding Role {registrationRole.Name}. Kick Stale Unregistered: {kickUnregistered}.").ConfigureAwait(false); + + await foreach (var userList in guild.GetUsersAsync(new RequestOptions { CancelToken = token }).ConfigureAwait(false)) + { + _logger.LogInformation("Processing chunk of {count} users, total processed: {proc}, total roles: {total}, roles added: {added}, users kicked: {kicked}, users plan to kick: {planToKick}, fresh user: {fresh}", + userList.Count, processedUsers, totalRoles + addedRoles, addedRoles, kickedUsers, toRemoveUsers, freshUsers); + foreach (var user in userList) + { + if (user.IsBot) continue; + + if (registeredUsers.Contains(user.Id)) + { + bool roleAdded = await _botServices.AddRegisteredRoleAsync(user, registrationRole).ConfigureAwait(false); + if (roleAdded) addedRoles++; + else totalRoles++; + } + else + { + if ((executionStartTime - user.JoinedAt.Value).TotalDays > 7) + { + if (kickUnregistered) + { + await _botServices.KickUserAsync(user).ConfigureAwait(false); + kickedUsers++; + } + else + { + toRemoveUsers++; + } + } + else + { + freshUsers++; + } + } + + token.ThrowIfCancellationRequested(); + processedUsers++; + } + } + + await _botServices.LogToChannel($"Processing registered users finished. Processed {processedUsers} users, added {addedRoles} roles and kicked {kickedUsers} users").ConfigureAwait(false); + } + + private async Task RemoveUsersNotInVanityRole(CancellationToken token) + { + var guild = (await _discordClient.Rest.GetGuildsAsync().ConfigureAwait(false)).First(); + + while (!token.IsCancellationRequested) + { + try + { + _logger.LogInformation($"Cleaning up Vanity UIDs"); + await _botServices.LogToChannel("Cleaning up Vanity UIDs").ConfigureAwait(false); + _logger.LogInformation("Getting rest guild {guildName}", guild.Name); + var restGuild = await _discordClient.Rest.GetGuildAsync(guild.Id).ConfigureAwait(false); + + Dictionary allowedRoleIds = _configurationService.GetValueOrDefault(nameof(ServicesConfiguration.VanityRoles), new Dictionary()); + _logger.LogInformation($"Allowed role ids: {string.Join(", ", allowedRoleIds)}"); + + if (allowedRoleIds.Any()) + { + using var db = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false); + + 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) + { + await CheckVanityForUser(restGuild, allowedRoleIds, db, lodestoneAuth, token).ConfigureAwait(false); + + await Task.Delay(1000, token).ConfigureAwait(false); + } + + foreach (var group in aliasedGroups) + { + await CheckVanityForGroup(restGuild, allowedRoleIds, db, group, token).ConfigureAwait(false); + + await Task.Delay(1000, token).ConfigureAwait(false); + } + } + 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), token).ConfigureAwait(false); + } + } + + private async Task CheckVanityForGroup(RestGuild restGuild, Dictionary allowedRoleIds, LightlessDbContext db, Group group, CancellationToken token) + { + var groupPrimaryUser = group.OwnerUID; + var groupOwner = await db.Auth.Include(u => u.User).SingleOrDefaultAsync(u => u.UserUID == group.OwnerUID).ConfigureAwait(false); + if (groupOwner != null && !string.IsNullOrEmpty(groupOwner.PrimaryUserUID)) + { + groupPrimaryUser = groupOwner.PrimaryUserUID; + } + + var lodestoneUser = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(f => f.User.UID == groupPrimaryUser).ConfigureAwait(false); + RestGuildUser discordUser = null; + if (lodestoneUser != null) + { + discordUser = await restGuild.GetUserAsync(lodestoneUser.DiscordId).ConfigureAwait(false); + } + + _logger.LogInformation($"Checking Group: {group.GID} [{group.Alias}], owned by {group.OwnerUID} ({groupPrimaryUser}), User in Roles: {string.Join(", ", discordUser?.RoleIds ?? new List())}"); + + if (lodestoneUser == null || discordUser == null || !discordUser.RoleIds.Any(allowedRoleIds.Keys.Contains)) + { + await _botServices.LogToChannel($"VANITY GID REMOVAL: <@{lodestoneUser?.DiscordId ?? 0}> ({lodestoneUser?.User?.UID}) - GID: {group.GID}, Vanity: {group.Alias}").ConfigureAwait(false); + + _logger.LogInformation($"User {lodestoneUser?.User?.UID ?? "unknown"} not in allowed roles, deleting group alias for {group.GID}"); + group.Alias = null; + db.Update(group); + await db.SaveChangesAsync(token).ConfigureAwait(false); + } + } + + private async Task CheckVanityForUser(RestGuild restGuild, Dictionary allowedRoleIds, LightlessDbContext db, LodeStoneAuth lodestoneAuth, CancellationToken token) + { + 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.Keys.Contains(u))) + { + _logger.LogInformation($"User {lodestoneAuth.User.UID} not in allowed roles, deleting alias"); + await _botServices.LogToChannel($"VANITY UID REMOVAL: <@{lodestoneAuth.DiscordId}> - UID: {lodestoneAuth.User.UID}, Vanity: {lodestoneAuth.User.Alias}").ConfigureAwait(false); + 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(token).ConfigureAwait(false); + } + } + + private async Task UpdateStatusAsync(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + var endPoint = _connectionMultiplexer.GetEndPoints().First(); + var onlineUsers = await _connectionMultiplexer.GetServer(endPoint).KeysAsync(pattern: "UID:*").CountAsync().ConfigureAwait(false); + + _logger.LogInformation("Users online: " + onlineUsers); + await _discordClient.SetActivityAsync(new Game("Lightless for " + onlineUsers + " Users")).ConfigureAwait(false); + await Task.Delay(TimeSpan.FromSeconds(10)).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncServices/Discord/DiscordBotServices.cs b/LightlessSyncServer/LightlessSyncServices/Discord/DiscordBotServices.cs new file mode 100644 index 0000000..1b54f89 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServices/Discord/DiscordBotServices.cs @@ -0,0 +1,175 @@ +using System.Collections.Concurrent; +using Discord; +using Discord.Net; +using Discord.Rest; +using Discord.WebSocket; +using LightlessSyncShared.Metrics; +using LightlessSyncShared.Models; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils.Configuration; +using StackExchange.Redis; + +namespace LightlessSyncServices.Discord; + +public class DiscordBotServices +{ + public readonly string[] LodestoneServers = ["eu", "na", "jp", "fr", "de"]; + public ConcurrentDictionary DiscordLodestoneMapping = new(); + public ConcurrentDictionary DiscordRelinkLodestoneMapping = new(); + public ConcurrentDictionary DiscordVerifiedUsers { get; } = new(); + public ConcurrentDictionary LastVanityChange = new(); + public ConcurrentDictionary LastVanityGidChange = new(StringComparer.Ordinal); + public ConcurrentDictionary ValidInteractions { get; } = new(); + public ConcurrentDictionary VanityRoles { get; set; } = new(); + public ConcurrentBag VerifiedCaptchaUsers { get; } = new(); + private readonly IConfigurationService _configuration; + private readonly CancellationTokenSource verificationTaskCts = new(); + private RestGuild? _guild; + private ulong? _logChannelId; + private RestTextChannel? _logChannel; + + public DiscordBotServices(ILogger logger, LightlessMetrics metrics, + IConfigurationService configuration) + { + Logger = logger; + Metrics = metrics; + _configuration = configuration; + } + + public ILogger Logger { get; init; } + public LightlessMetrics Metrics { get; init; } + public ConcurrentQueue>> VerificationQueue { get; } = new(); + + public Task Start() + { + _ = ProcessVerificationQueue(); + return Task.CompletedTask; + } + + public Task Stop() + { + verificationTaskCts.Cancel(); + verificationTaskCts.Dispose(); + return Task.CompletedTask; + } + + public async Task LogToChannel(string msg) + { + if (_guild == null) return; + Logger.LogInformation("LogToChannel: {msg}", msg); + var logChannelId = _configuration.GetValueOrDefault(nameof(ServicesConfiguration.DiscordChannelForBotLog), null); + if (logChannelId == null) return; + if (logChannelId != _logChannelId) + { + try + { + _logChannelId = logChannelId; + _logChannel = await _guild.GetTextChannelAsync(logChannelId.Value).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogError(ex, "Could not get bot log channel"); + } + } + + if (_logChannel == null) return; + await _logChannel.SendMessageAsync(msg).ConfigureAwait(false); + } + + private async Task RetryAsync(Task action, IUser user, string operation, bool logInfoToChannel = true) + { + int retryCount = 0; + int maxRetries = 5; + var retryDelay = TimeSpan.FromSeconds(5); + + while (retryCount < maxRetries) + { + try + { + await action.ConfigureAwait(false); + if (logInfoToChannel) + await LogToChannel($"{user.Mention} {operation} SUCCESS").ConfigureAwait(false); + break; + } + catch (RateLimitedException) + { + retryCount++; + await LogToChannel($"{user.Mention} {operation} RATELIMIT, retry {retryCount} in {retryDelay}.").ConfigureAwait(false); + await Task.Delay(retryDelay).ConfigureAwait(false); + } + catch (Exception ex) + { + await LogToChannel($"{user.Mention} {operation} FAILED: {ex.Message}").ConfigureAwait(false); + break; + } + } + + if (retryCount == maxRetries) + { + await LogToChannel($"{user.Mention} FAILED: RetryCount exceeded.").ConfigureAwait(false); + } + } + + public async Task RemoveRegisteredRoleAsync(IUser user) + { + var registeredRole = _configuration.GetValueOrDefault(nameof(ServicesConfiguration.DiscordRoleRegistered), null); + if (registeredRole == null) return; + var restUser = await _guild.GetUserAsync(user.Id).ConfigureAwait(false); + if (restUser == null) return; + if (!restUser.RoleIds.Contains(registeredRole.Value)) return; + await RetryAsync(restUser.RemoveRoleAsync(registeredRole.Value), user, $"Remove Registered Role").ConfigureAwait(false); + } + + public async Task AddRegisteredRoleAsync(IUser user) + { + var registeredRole = _configuration.GetValueOrDefault(nameof(ServicesConfiguration.DiscordRoleRegistered), null); + if (registeredRole == null) return; + var restUser = await _guild.GetUserAsync(user.Id).ConfigureAwait(false); + if (restUser == null) return; + if (restUser.RoleIds.Contains(registeredRole.Value)) return; + await RetryAsync(restUser.AddRoleAsync(registeredRole.Value), user, $"Add Registered Role").ConfigureAwait(false); + } + + public async Task AddRegisteredRoleAsync(RestGuildUser user, RestRole role) + { + if (user.RoleIds.Contains(role.Id)) return false; + await RetryAsync(user.AddRoleAsync(role), user, $"Add Registered Role", false).ConfigureAwait(false); + return true; + } + + public async Task KickUserAsync(RestGuildUser user) + { + await RetryAsync(user.KickAsync("No registration found"), user, "Kick").ConfigureAwait(false); + } + + private async Task ProcessVerificationQueue() + { + while (!verificationTaskCts.IsCancellationRequested) + { + Logger.LogDebug("Processing Verification Queue, Entries: {entr}", VerificationQueue.Count); + if (VerificationQueue.TryPeek(out var queueitem)) + { + try + { + await queueitem.Value.Invoke(this).ConfigureAwait(false); + Logger.LogInformation("Processed Verification for {key}", queueitem.Key); + } + catch (Exception e) + { + Logger.LogError(e, "Error during queue work"); + } + finally + { + VerificationQueue.TryDequeue(out _); + } + } + + await Task.Delay(TimeSpan.FromSeconds(2), verificationTaskCts.Token).ConfigureAwait(false); + } + } + + internal void UpdateGuild(RestGuild guild) + { + _guild = guild; + } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncServices/Discord/LodestoneModal.cs b/LightlessSyncServer/LightlessSyncServices/Discord/LodestoneModal.cs new file mode 100644 index 0000000..fb94a20 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServices/Discord/LodestoneModal.cs @@ -0,0 +1,15 @@ +using Discord; +using Discord.Interactions; + +namespace LightlessSyncServices.Discord; + +// todo: remove all this crap at some point + +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; } +} diff --git a/LightlessSyncServer/LightlessSyncServices/Discord/MareModule.cs b/LightlessSyncServer/LightlessSyncServices/Discord/MareModule.cs new file mode 100644 index 0000000..05460e3 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServices/Discord/MareModule.cs @@ -0,0 +1,293 @@ +using Discord; +using Discord.Interactions; +using LightlessSyncShared.Data; +using Microsoft.EntityFrameworkCore; +using Prometheus; +using LightlessSyncShared.Models; +using LightlessSyncShared.Utils; +using LightlessSyncShared.Services; +using StackExchange.Redis; +using LightlessSync.API.Data.Enum; +using LightlessSyncShared.Utils.Configuration; + +namespace LightlessSyncServices.Discord; + +public class LightlessModule : InteractionModuleBase +{ + private readonly ILogger _logger; + private readonly IServiceProvider _services; + private readonly IConfigurationService _lightlessServicesConfiguration; + private readonly IConnectionMultiplexer _connectionMultiplexer; + + public LightlessModule(ILogger logger, IServiceProvider services, + IConfigurationService lightlessServicesConfiguration, + IConnectionMultiplexer connectionMultiplexer) + { + _logger = logger; + _services = services; + _lightlessServicesConfiguration = lightlessServicesConfiguration; + _connectionMultiplexer = connectionMultiplexer; + } + + [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("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(); + await c.PostAsJsonAsync(new Uri(_lightlessServicesConfiguration.GetValue + (nameof(ServicesConfiguration.MainServerAddress)), "/msgc/sendMessage"), new ClientMessage(messageType, message, uid ?? string.Empty)) + .ConfigureAwait(false); + + var discordChannelForMessages = _lightlessServicesConfiguration.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); + } + } + + 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 Lightless 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 Lightless 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; + } +} diff --git a/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.AprilFools2024.cs b/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.AprilFools2024.cs new file mode 100644 index 0000000..25ae39f --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.AprilFools2024.cs @@ -0,0 +1,215 @@ +using Discord; +using Discord.Interactions; +using LightlessSyncShared.Utils.Configuration; +using System.Text.Json; + +namespace LightlessSyncServices.Discord; + +public partial class LightlessWizardModule : InteractionModuleBase +{ + private const int _totalAprilFoolsRoles = 200; + private const string _persistentFileName = "april2024.json"; + + private static readonly SemaphoreSlim _fileSemaphore = new(1, 1); + + [ComponentInteraction("wizard-fools")] + public async Task ComponentFools() + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}", nameof(ComponentFools), Context.Interaction.User.Id); + + EmbedBuilder eb = new(); + eb.WithTitle("WorryCoin™ and LightlessToken© Balance"); + eb.WithColor(Color.Gold); + eb.WithDescription("You currently have" + Environment.NewLine + Environment.NewLine + + "**200000** MaTE©" + Environment.NewLine + + "**0** WorryCoin™" + Environment.NewLine + Environment.NewLine + + "You have no payment method set up. Press the button below to add a payment method."); + ComponentBuilder cb = new(); + AddHome(cb); + cb.WithButton("Add Payment Method", "wizard-fools-start", ButtonStyle.Primary, emote: new Emoji("💲")); + await ModifyInteraction(eb, cb).ConfigureAwait(false); + } + + [ComponentInteraction("wizard-fools-start")] + public async Task ComponentFoolsStart() + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}", nameof(ComponentFoolsStart), Context.Interaction.User.Id); + + EmbedBuilder eb = new(); + var user = await Context.Guild.GetUserAsync(Context.User.Id).ConfigureAwait(false); + bool userIsInPermanentVanityRole = _botServices.VanityRoles.Where(v => !v.Value.Contains('$', StringComparison.Ordinal)) + .Select(v => v.Key).Any(u => user.RoleIds.Contains(u.Id)) || !_botServices.VanityRoles.Any(); + ComponentBuilder cb = new(); + AddHome(cb); + + var participatedUsers = await GetParticipants().ConfigureAwait(false); + var remainingRoles = _totalAprilFoolsRoles - participatedUsers.Count(c => c.Value == true); + + if (userIsInPermanentVanityRole) + { + eb.WithColor(Color.Green); + eb.WithTitle("Happy April Fools!"); + eb.WithDescription("Thank you for participating in Lightlesss 2024 April Fools event." + + Environment.NewLine + Environment.NewLine + + "As you might have already guessed from the post, nothing that was written there had any truth behind it." + + Environment.NewLine + Environment.NewLine + + "This entire thing was a jab at the ridiculousness of cryptocurrency, microtransactions and games featuring multiple currencies. I hope you enjoyed the announcement post!" + + Environment.NewLine + Environment.NewLine + + "__As you already have a role that gives you a permanent Vanity ID, you cannot win another one here. " + + "However, tell your friends as this bot will give them a chance to win one of " + _totalAprilFoolsRoles + " lifetime vanity roles.__" + + Environment.NewLine + Environment.NewLine + + "The giveaway is active until ."); + } + else if (participatedUsers.ContainsKey(Context.User.Id)) + { + eb.WithColor(Color.Orange); + eb.WithTitle("Happy April Fools!"); + eb.WithDescription("Thank you for participating in Lightlesss 2024 April Fools event." + + Environment.NewLine + Environment.NewLine + + "As you might have already guessed from the post, nothing that was written there had any truth behind it." + + Environment.NewLine + Environment.NewLine + + "This entire thing was a jab at the ridiculousness of cryptocurrency, microtransactions and games featuring multiple currencies. I hope you enjoyed the announcement post!" + + Environment.NewLine + Environment.NewLine + + "__You already participated in the giveaway of the permanent Vanity roles and therefore cannot participate again. Better luck next time!__"); + } + else if (remainingRoles > 0) + { + eb.WithColor(Color.Green); + eb.WithTitle("Happy April Fools!"); + eb.WithDescription("Thank you for participating in Lightlesss 2024 April Fools event." + + Environment.NewLine + Environment.NewLine + + "As you might have already guessed from the post, nothing that was written there had any truth behind it." + + Environment.NewLine + Environment.NewLine + + "This entire thing was a jab at the ridiculousness of cryptocurrency, microtransactions and games featuring multiple currencies. I hope you enjoyed the announcement post!" + + Environment.NewLine + Environment.NewLine + + "You have currently no permanent role that allows you to set a Vanity ID, however I am giving away a total of " + _totalAprilFoolsRoles + " permanent vanity roles " + + "(" + remainingRoles + " still remain) and you can win one using this bot!" + + Environment.NewLine + Environment.NewLine + + "To win you simply have to pick one of the buttons labeled \"Win\" below this post. Which button will win is random. " + + "There is a 1 in 5 chance that you can win the role. __You can only participate once.__" + + Environment.NewLine + Environment.NewLine + + "The giveaway is active until ."); + cb.WithButton("Win", "wizard-fools-win:1", ButtonStyle.Primary, new Emoji("1️⃣")); + cb.WithButton("Win", "wizard-fools-win:2", ButtonStyle.Primary, new Emoji("2️⃣")); + cb.WithButton("Win", "wizard-fools-win:3", ButtonStyle.Primary, new Emoji("3️⃣")); + cb.WithButton("Win", "wizard-fools-win:4", ButtonStyle.Primary, new Emoji("4️⃣")); + cb.WithButton("Win", "wizard-fools-win:5", ButtonStyle.Primary, new Emoji("5️⃣")); + } + else + { + eb.WithColor(Color.Orange); + eb.WithTitle("Happy April Fools!"); + eb.WithDescription("Thank you for participating in Lightlesss 2024 April Fools event." + + Environment.NewLine + Environment.NewLine + + "As you might have already guessed from the post, nothing that was written there had any truth behind it." + + Environment.NewLine + Environment.NewLine + + "This entire thing was a jab at the ridiculousness of cryptocurrency, microtransactions and games featuring multiple currencies. I hope you enjoyed the announcement post!" + + Environment.NewLine + Environment.NewLine + + "__I have been giving away " + _totalAprilFoolsRoles + " permanent Vanity ID roles for this server, however you are sadly too late as they ran out by now. " + + "Better luck next year with whatever I will come up with!__"); + } + await ModifyInteraction(eb, cb).ConfigureAwait(false); + } + + [ComponentInteraction("wizard-fools-win:*")] + public async Task ComponentFoolsWin(int number) + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}", nameof(ComponentFoolsWin), Context.Interaction.User.Id); + + var winningNumber = new Random().Next(1, 6); + EmbedBuilder eb = new(); + ComponentBuilder cb = new(); + AddHome(cb); + bool hasWon = winningNumber == number; + + await WriteParticipants(Context.Interaction.User.Id, hasWon).ConfigureAwait(false); + + if (hasWon) + { + eb.WithColor(Color.Gold); + eb.WithTitle("Congratulations you are winner!"); + eb.WithDescription("You, by pure accident and sheer luck, picked the right number and have won yourself a lifetime Vanity ID role on this server!" + + Environment.NewLine + Environment.NewLine + + "The role will remain as long as you remain on this server, if you happen to leave it you will not get the role back." + + Environment.NewLine + Environment.NewLine + + "Head over to Home and to the Vanity IDs section to set it up for your account!" + + Environment.NewLine + Environment.NewLine + + "Once again, thank you for participating and have a great day."); + + var user = await Context.Guild.GetUserAsync(Context.User.Id).ConfigureAwait(false); + await user.AddRoleAsync(_lightlessServicesConfiguration.GetValue(nameof(ServicesConfiguration.DiscordRoleAprilFools2024)).Value).ConfigureAwait(false); + } + else + { + eb.WithColor(Color.Red); + eb.WithTitle("Fortune did not bless you"); + eb.WithDescription("You, through sheer misfortune, sadly did not pick the right number. (The winning number was " + winningNumber + ")" + + Environment.NewLine + Environment.NewLine + + "Better luck next time!" + + Environment.NewLine + Environment.NewLine + + "Once again, thank you for participating and regardless, have a great day."); + } + + await ModifyInteraction(eb, cb).ConfigureAwait(false); + } + + private async Task> GetParticipants() + { + await _fileSemaphore.WaitAsync().ConfigureAwait(false); + + try + { + if (!File.Exists(_persistentFileName)) + { + return new(); + } + + var json = await File.ReadAllTextAsync(_persistentFileName).ConfigureAwait(false); + return JsonSerializer.Deserialize>(json); + } + catch + { + return new(); + } + finally + { + _fileSemaphore.Release(); + } + } + + private async Task WriteParticipants(ulong participant, bool win) + { + await _fileSemaphore.WaitAsync().ConfigureAwait(false); + + try + { + Dictionary participants = new(); + if (File.Exists(_persistentFileName)) + { + try + { + var json = await File.ReadAllTextAsync(_persistentFileName).ConfigureAwait(false); + participants = JsonSerializer.Deserialize>(json); + } + catch + { + // probably empty file just deal with it + } + } + + participants[participant] = win; + + await File.WriteAllTextAsync(_persistentFileName, JsonSerializer.Serialize(participants)).ConfigureAwait(false); + } + finally + { + _fileSemaphore.Release(); + } + } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Delete.cs b/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Delete.cs new file mode 100644 index 0000000..af5e682 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Delete.cs @@ -0,0 +1,119 @@ +using Discord.Interactions; +using Discord; +using LightlessSyncShared.Utils; +using LightlessSyncShared.Utils.Configuration; +using Discord.WebSocket; +using Microsoft.EntityFrameworkCore; + +namespace LightlessSyncServices.Discord; + +public partial class LightlessWizardModule +{ + [ComponentInteraction("wizard-delete")] + public async Task ComponentDelete() + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}", nameof(ComponentDelete), Context.Interaction.User.Id); + + using var lightlessDb = await GetDbContext().ConfigureAwait(false); + EmbedBuilder eb = new(); + eb.WithTitle("Delete Account"); + eb.WithDescription("You can delete your primary or secondary UIDs here." + Environment.NewLine + Environment.NewLine + + "__Note: deleting your primary UID will delete all associated secondary UIDs as well.__" + Environment.NewLine + Environment.NewLine + + "- 1️⃣ is your primary account/UID" + Environment.NewLine + + "- 2️⃣ are all your secondary accounts/UIDs" + Environment.NewLine + + "If you are using Vanity UIDs the original UID is displayed in the second line of the account selection."); + eb.WithColor(Color.Blue); + + ComponentBuilder cb = new(); + await AddUserSelection(lightlessDb, cb, "wizard-delete-select").ConfigureAwait(false); + AddHome(cb); + await ModifyInteraction(eb, cb).ConfigureAwait(false); + } + + [ComponentInteraction("wizard-delete-select")] + public async Task SelectionDeleteAccount(string uid) + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}:{uid}", nameof(SelectionDeleteAccount), Context.Interaction.User.Id, uid); + + using var lightlessDb = await GetDbContext().ConfigureAwait(false); + bool isPrimary = lightlessDb.Auth.Single(u => u.UserUID == uid).PrimaryUserUID == null; + EmbedBuilder eb = new(); + eb.WithTitle($"Are you sure you want to delete {uid}?"); + eb.WithDescription($"This operation is irreversible. All your pairs, joined syncshells and information stored on the service for {uid} will be " + + $"irrevocably deleted." + + (isPrimary ? (Environment.NewLine + Environment.NewLine + + "⚠️ **You are about to delete a Primary UID, all attached Secondary UIDs and their information will be deleted as well.** ⚠️") : string.Empty)); + eb.WithColor(Color.Purple); + ComponentBuilder cb = new(); + cb.WithButton("Cancel", "wizard-delete", emote: new Emoji("❌")); + cb.WithButton($"Delete {uid}", "wizard-delete-confirm:" + uid, ButtonStyle.Danger, emote: new Emoji("🗑️")); + await ModifyInteraction(eb, cb).ConfigureAwait(false); + } + + [ComponentInteraction("wizard-delete-confirm:*")] + public async Task ComponentDeleteAccountConfirm(string uid) + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}:{uid}", nameof(ComponentDeleteAccountConfirm), Context.Interaction.User.Id, uid); + + await RespondWithModalAsync("wizard-delete-confirm-modal:" + uid).ConfigureAwait(false); + } + + [ModalInteraction("wizard-delete-confirm-modal:*")] + public async Task ModalDeleteAccountConfirm(string uid, ConfirmDeletionModal modal) + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}:{uid}", nameof(ModalDeleteAccountConfirm), Context.Interaction.User.Id, uid); + + try + { + if (!string.Equals("DELETE", modal.Delete, StringComparison.Ordinal)) + { + EmbedBuilder eb = new(); + eb.WithTitle("Did not confirm properly"); + eb.WithDescription($"You entered {modal.Delete} but requested was DELETE. Please try again and enter DELETE to confirm."); + eb.WithColor(Color.Red); + ComponentBuilder cb = new(); + cb.WithButton("Cancel", "wizard-delete", emote: new Emoji("❌")); + cb.WithButton("Retry", "wizard-delete-confirm:" + uid, emote: new Emoji("🔁")); + + await ModifyModalInteraction(eb, cb).ConfigureAwait(false); + } + else + { + var maxGroupsByUser = _lightlessClientConfigurationService.GetValueOrDefault(nameof(ServerConfiguration.MaxGroupUserCount), 3); + + using var db = await GetDbContext().ConfigureAwait(false); + var user = await db.Users.SingleAsync(u => u.UID == uid).ConfigureAwait(false); + var lodestone = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.User.UID == uid).ConfigureAwait(false); + await SharedDbFunctions.PurgeUser(_logger, user, db, maxGroupsByUser).ConfigureAwait(false); + + EmbedBuilder eb = new(); + eb.WithTitle($"Account {uid} successfully deleted"); + eb.WithColor(Color.Green); + ComponentBuilder cb = new(); + AddHome(cb); + + await ModifyModalInteraction(eb, cb).ConfigureAwait(false); + + await _botServices.LogToChannel($"{Context.User.Mention} DELETE SUCCESS: {uid}").ConfigureAwait(false); + + // only remove role if deleted uid has lodestone attached (== primary uid) + if (lodestone != null) + { + await _botServices.RemoveRegisteredRoleAsync(Context.Interaction.User).ConfigureAwait(false); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error handling modal delete account confirm"); + } + } +} diff --git a/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Recover.cs b/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Recover.cs new file mode 100644 index 0000000..95a5f93 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Recover.cs @@ -0,0 +1,90 @@ +using Discord.Interactions; +using Discord; +using LightlessSyncShared.Data; +using LightlessSyncShared.Models; +using LightlessSyncShared.Utils; +using Microsoft.EntityFrameworkCore; + +namespace LightlessSyncServices.Discord; + +public partial class LightlessWizardModule +{ + [ComponentInteraction("wizard-recover")] + public async Task ComponentRecover() + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}", nameof(ComponentRecover), Context.Interaction.User.Id); + + using var lightlessDb = await GetDbContext().ConfigureAwait(false); + EmbedBuilder eb = new(); + eb.WithColor(Color.Blue); + eb.WithTitle("Recover"); + eb.WithDescription("In case you have lost your secret key you can recover it here." + Environment.NewLine + Environment.NewLine + + "## ⚠️ **Once you recover your key, the previously used key will be invalidated. If you use Lightless on multiple devices you will have to update the key everywhere you use it.** ⚠️" + Environment.NewLine + Environment.NewLine + + "Use the selection below to select the user account you want to recover." + Environment.NewLine + Environment.NewLine + + "- 1️⃣ is your primary account/UID" + Environment.NewLine + + "- 2️⃣ are all your secondary accounts/UIDs" + Environment.NewLine + + "If you are using Vanity UIDs the original UID is displayed in the second line of the account selection." + Environment.NewLine + + "# Note: instead of recovery and handling secret keys the switch to OAuth2 authentication is strongly suggested."); + ComponentBuilder cb = new(); + await AddUserSelection(lightlessDb, cb, "wizard-recover-select").ConfigureAwait(false); + AddHome(cb); + await ModifyInteraction(eb, cb).ConfigureAwait(false); + } + + [ComponentInteraction("wizard-recover-select")] + public async Task SelectionRecovery(string uid) + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}:{uid}", nameof(SelectionRecovery), Context.Interaction.User.Id, uid); + + using var lightlessDb = await GetDbContext().ConfigureAwait(false); + EmbedBuilder eb = new(); + eb.WithColor(Color.Green); + await HandleRecovery(lightlessDb, eb, uid).ConfigureAwait(false); + ComponentBuilder cb = new(); + AddHome(cb); + await ModifyInteraction(eb, cb).ConfigureAwait(false); + } + + private async Task HandleRecovery(LightlessDbContext db, EmbedBuilder embed, string uid) + { + string computedHash = string.Empty; + Auth auth; + var previousAuth = await db.Auth.Include(u => u.User).FirstOrDefaultAsync(u => u.UserUID == uid).ConfigureAwait(false); + if (previousAuth != null) + { + db.Auth.Remove(previousAuth); + } + + computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString()); + string hashedKey = StringUtils.Sha256String(computedHash); + auth = new Auth() + { + HashedKey = hashedKey, + User = previousAuth.User, + PrimaryUserUID = previousAuth.PrimaryUserUID + }; + + await db.Auth.AddAsync(auth).ConfigureAwait(false); + + embed.WithTitle($"Recovery for {uid} complete"); + 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 + + "**__NOTE: Secret keys are considered legacy authentication. If you are using the suggested OAuth2 authentication, you do not need to use the Secret Key or recover ever again.__**" + + Environment.NewLine + Environment.NewLine + + $"||**`{computedHash}`**||" + + Environment.NewLine + + "__NOTE: The Secret Key only contains the letters ABCDEF and numbers 0 - 9.__" + + Environment.NewLine + Environment.NewLine + + "Enter this key in the Lightless Sync Service Settings and reconnect to the service."); + + await db.Auth.AddAsync(auth).ConfigureAwait(false); + await db.SaveChangesAsync().ConfigureAwait(false); + + _botServices.Logger.LogInformation("User recovered: {userUID}:{hashedKey}", previousAuth.UserUID, hashedKey); + await _botServices.LogToChannel($"{Context.User.Mention} RECOVER SUCCESS: {previousAuth.UserUID}").ConfigureAwait(false); + } +} diff --git a/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Register.cs b/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Register.cs new file mode 100644 index 0000000..5250030 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Register.cs @@ -0,0 +1,311 @@ +using Discord.Interactions; +using Discord; +using LightlessSyncShared.Data; +using Microsoft.EntityFrameworkCore; +using LightlessSyncShared.Utils; +using LightlessSyncShared.Models; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils.Configuration; +using Discord.Rest; +using Discord.WebSocket; + +namespace LightlessSyncServices.Discord; + +public partial class LightlessWizardModule +{ + [ComponentInteraction("wizard-register")] + public async Task ComponentRegister() + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}", nameof(ComponentRegister), Context.Interaction.User.Id); + + EmbedBuilder eb = new(); + eb.WithColor(Color.Blue); + eb.WithTitle("Start Registration"); + eb.WithDescription("Here you can start the registration process with the Lightless Sync server of this Discord." + Environment.NewLine + Environment.NewLine + + "- Have your Lodestone URL ready (i.e. https://eu.finalfantasyxiv.com/lodestone/character/XXXXXXXXX)" + Environment.NewLine + + " - The registration requires you to modify your Lodestone profile with a generated code for verification" + Environment.NewLine + + "- Do not use this on mobile because you will need to be able to copy the generated secret key" + Environment.NewLine + + "# Follow the bot instructions precisely. Slow down and read."); + ComponentBuilder cb = new(); + AddHome(cb); + cb.WithButton("Start Registration", "wizard-register-start", ButtonStyle.Primary, emote: new Emoji("🌒")); + await ModifyInteraction(eb, cb).ConfigureAwait(false); + } + + [ComponentInteraction("wizard-register-start")] + public async Task ComponentRegisterStart() + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}", nameof(ComponentRegisterStart), Context.Interaction.User.Id); + + using var db = await GetDbContext().ConfigureAwait(false); + var entry = await db.LodeStoneAuth.SingleOrDefaultAsync(u => u.DiscordId == Context.User.Id && u.StartedAt != null).ConfigureAwait(false); + if (entry != null) + { + db.LodeStoneAuth.Remove(entry); + } + _botServices.DiscordLodestoneMapping.TryRemove(Context.User.Id, out _); + _botServices.DiscordVerifiedUsers.TryRemove(Context.User.Id, out _); + + await db.SaveChangesAsync().ConfigureAwait(false); + + await RespondWithModalAsync("wizard-register-lodestone-modal").ConfigureAwait(false); + } + + [ModalInteraction("wizard-register-lodestone-modal")] + public async Task ModalRegister(LodestoneModal lodestoneModal) + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}:{lodestone}", nameof(ModalRegister), Context.Interaction.User.Id, lodestoneModal.LodestoneUrl); + + EmbedBuilder eb = new(); + eb.WithColor(Color.Purple); + var success = await HandleRegisterModalAsync(eb, lodestoneModal).ConfigureAwait(false); + ComponentBuilder cb = new(); + cb.WithButton("Cancel", "wizard-register", ButtonStyle.Secondary, emote: new Emoji("❌")); + if (success.Item1) cb.WithButton("Verify", "wizard-register-verify:" + success.Item2, ButtonStyle.Primary, emote: new Emoji("✅")); + else cb.WithButton("Try again", "wizard-register-start", ButtonStyle.Primary, emote: new Emoji("🔁")); + await ModifyModalInteraction(eb, cb).ConfigureAwait(false); + } + + [ComponentInteraction("wizard-register-verify:*")] + public async Task ComponentRegisterVerify(string verificationCode) + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}:{verificationcode}", nameof(ComponentRegisterVerify), Context.Interaction.User.Id, verificationCode); + + _botServices.VerificationQueue.Enqueue(new KeyValuePair>(Context.User.Id, + (service) => HandleVerifyAsync(Context.User.Id, verificationCode, service))); + EmbedBuilder eb = new(); + ComponentBuilder cb = new(); + eb.WithColor(Color.Purple); + cb.WithButton("Cancel", "wizard-register", ButtonStyle.Secondary, emote: new Emoji("❌")); + cb.WithButton("Check", "wizard-register-verify-check:" + verificationCode, ButtonStyle.Primary, emote: new Emoji("❓")); + eb.WithTitle("Verification Pending"); + eb.WithDescription("Please wait until the bot verifies your registration." + Environment.NewLine + + "Press \"Check\" to check if the verification has been already processed" + Environment.NewLine + Environment.NewLine + + "__This will not advance automatically, you need to press \"Check\".__"); + await ModifyInteraction(eb, cb).ConfigureAwait(false); + } + + [ComponentInteraction("wizard-register-verify-check:*")] + public async Task ComponentRegisterVerifyCheck(string verificationCode) + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}:{uid}", nameof(ComponentRegisterVerifyCheck), Context.Interaction.User.Id, verificationCode); + + EmbedBuilder eb = new(); + ComponentBuilder cb = new(); + bool stillEnqueued = _botServices.VerificationQueue.Any(k => k.Key == Context.User.Id); + bool verificationRan = _botServices.DiscordVerifiedUsers.TryGetValue(Context.User.Id, out bool verified); + bool registerSuccess = false; + if (!verificationRan) + { + if (stillEnqueued) + { + eb.WithColor(Color.Gold); + eb.WithTitle("Your verification is still pending"); + eb.WithDescription("Please try again and click Check in a few seconds"); + cb.WithButton("Cancel", "wizard-register", ButtonStyle.Secondary, emote: new Emoji("❌")); + cb.WithButton("Check", "wizard-register-verify-check:" + verificationCode, ButtonStyle.Primary, emote: new Emoji("❓")); + } + else + { + eb.WithColor(Color.Red); + eb.WithTitle("Something went wrong"); + eb.WithDescription("Your verification was processed but did not arrive properly. Please try to start the registration from the start."); + cb.WithButton("Restart", "wizard-register", ButtonStyle.Primary, emote: new Emoji("🔁")); + } + } + else + { + if (verified) + { + eb.WithColor(Color.Green); + using var db = await GetDbContext().ConfigureAwait(false); + var (uid, key) = await HandleAddUser(db).ConfigureAwait(false); + eb.WithTitle($"Registration successful, your UID: {uid}"); + 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 + + "**__NOTE: Secret keys are considered legacy. Using the suggested OAuth2 authentication in Lightless, you do not need to use this Secret Key.__**" + + Environment.NewLine + Environment.NewLine + + $"||**`{key}`**||" + + Environment.NewLine + Environment.NewLine + + "If you want to continue using legacy authentication, enter this key in Lightless Sync and hit save to connect to the service." + + Environment.NewLine + + "__NOTE: The Secret Key only contains the letters ABCDEF and numbers 0 - 9.__" + + Environment.NewLine + + "You should connect as soon as possible to not get caught by the automatic cleanup process." + + Environment.NewLine + + "Have fun."); + AddHome(cb); + registerSuccess = true; + } + else + { + eb.WithColor(Color.Gold); + eb.WithTitle("Failed to verify registration"); + eb.WithDescription("The bot was not able to find the required verification code on your Lodestone profile." + + Environment.NewLine + Environment.NewLine + + "Please restart your verification process, make sure to save your profile _twice_ for it to be properly saved." + + Environment.NewLine + Environment.NewLine + + "If this link does not lead to your profile edit page, you __need__ to configure the privacy settings first: https://na.finalfantasyxiv.com/lodestone/my/setting/profile/" + + Environment.NewLine + Environment.NewLine + + "**Make sure your profile is set to public (All Users) for your character. The bot cannot read profiles with privacy settings set to \"logged in\" or \"private\".**" + + Environment.NewLine + Environment.NewLine + + "## You __need__ to enter following the code this bot provided onto your Lodestone in the character profile:" + + Environment.NewLine + Environment.NewLine + + "**`" + verificationCode + "`**"); + cb.WithButton("Cancel", "wizard-register", emote: new Emoji("❌")); + cb.WithButton("Retry", "wizard-register-verify:" + verificationCode, ButtonStyle.Primary, emote: new Emoji("🔁")); + } + } + + await ModifyInteraction(eb, cb).ConfigureAwait(false); + if (registerSuccess) + await _botServices.AddRegisteredRoleAsync(Context.Interaction.User).ConfigureAwait(false); + } + + private async Task<(bool, string)> HandleRegisterModalAsync(EmbedBuilder embed, LodestoneModal arg) + { + 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/"); + return (false, string.Empty); + } + + // check if userid is already in db + var hashedLodestoneId = StringUtils.Sha256String(lodestoneId.ToString()); + + using var db = await GetDbContext().ConfigureAwait(false); + + // check if discord id or lodestone id is banned + if (db.BannedRegistrations.Any(a => a.DiscordIdOrLodestoneAuth == hashedLodestoneId)) + { + embed.WithDescription("This account is banned"); + return (false, string.Empty); + } + + if (db.LodeStoneAuth.Any(a => a.HashedLodestoneId == hashedLodestoneId)) + { + // character already in db + embed.WithDescription("This lodestone character already exists in the Database. If you want to attach this character to your current Discord account use relink."); + return (false, string.Empty); + } + + string lodestoneAuth = await GenerateLodestoneAuth(Context.User.Id, 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 + + "__NOTE: If the link does not lead you to your character edit profile page, you need to log in and set up your privacy settings!__" + + Environment.NewLine + Environment.NewLine + + $"**`{lodestoneAuth}`**" + + Environment.NewLine + Environment.NewLine + + $"**! THIS IS NOT THE KEY YOU HAVE TO ENTER IN LIGHTLESS !**" + + Environment.NewLine + Environment.NewLine + + "Once added and saved, use the button below to Verify and finish registration and receive a secret key to use for Lightless Sync." + + 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[Context.User.Id] = lodestoneId.ToString(); + + return (true, lodestoneAuth); + } + + private async Task HandleVerifyAsync(ulong userid, string authString, DiscordBotServices services) + { + using var req = new HttpClient(); + + services.DiscordVerifiedUsers.Remove(userid, out _); + if (services.DiscordLodestoneMapping.ContainsKey(userid)) + { + var randomServer = services.LodestoneServers[random.Next(services.LodestoneServers.Length)]; + var url = $"https://{randomServer}.finalfantasyxiv.com/lodestone/character/{services.DiscordLodestoneMapping[userid]}"; + using var response = await req.GetAsync(url).ConfigureAwait(false); + _logger.LogInformation("Verifying {userid} with URL {url}", userid, url); + if (response.IsSuccessStatusCode || response.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + if (content.Contains(authString)) + { + services.DiscordVerifiedUsers[userid] = true; + _logger.LogInformation("Verified {userid} from lodestone {lodestone}", userid, services.DiscordLodestoneMapping[userid]); + await _botServices.LogToChannel($"<@{userid}> REGISTER VERIFY: Success.").ConfigureAwait(false); + services.DiscordLodestoneMapping.TryRemove(userid, out _); + } + else + { + services.DiscordVerifiedUsers[userid] = false; + _logger.LogInformation("Could not verify {userid} from lodestone {lodestone}, did not find authString: {authString}, status code was: {code}", + userid, services.DiscordLodestoneMapping[userid], authString, response.StatusCode); + await _botServices.LogToChannel($"<@{userid}> REGISTER VERIFY: Failed: No Authstring ({authString}). (<{url}>)").ConfigureAwait(false); + } + } + else + { + _logger.LogWarning("Could not verify {userid}, HttpStatusCode: {code}", userid, response.StatusCode); + await _botServices.LogToChannel($"<@{userid}> REGISTER VERIFY: Failed: HttpStatusCode {response.StatusCode}. (<{url}>)").ConfigureAwait(false); + } + } + } + + private async Task<(string, string)> HandleAddUser(LightlessDbContext db) + { + var lodestoneAuth = db.LodeStoneAuth.SingleOrDefault(u => u.DiscordId == Context.User.Id); + + var user = new User(); + + var hasValidUid = false; + while (!hasValidUid) + { + var uid = StringUtils.GenerateRandomString(10); + 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()); + string hashedKey = StringUtils.Sha256String(computedHash); + var auth = new Auth() + { + HashedKey = hashedKey, + User = user, + }; + + await db.Users.AddAsync(user).ConfigureAwait(false); + await db.Auth.AddAsync(auth).ConfigureAwait(false); + + lodestoneAuth.StartedAt = null; + lodestoneAuth.User = user; + lodestoneAuth.LodestoneAuthString = null; + + await db.SaveChangesAsync().ConfigureAwait(false); + + _botServices.Logger.LogInformation("User registered: {userUID}:{hashedKey}", user.UID, hashedKey); + + await _botServices.LogToChannel($"{Context.User.Mention} REGISTER COMPLETE: => {user.UID}").ConfigureAwait(false); + + _botServices.DiscordVerifiedUsers.Remove(Context.User.Id, out _); + + return (user.UID, computedHash); + } +} diff --git a/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Relink.cs b/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Relink.cs new file mode 100644 index 0000000..d746c93 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Relink.cs @@ -0,0 +1,281 @@ +using Discord.Interactions; +using Discord; +using LightlessSyncShared.Data; +using LightlessSyncShared.Utils; +using LightlessSyncShared.Models; +using Microsoft.EntityFrameworkCore; + +namespace LightlessSyncServices.Discord; + +public partial class LightlessWizardModule +{ + [ComponentInteraction("wizard-relink")] + public async Task ComponentRelink() + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}", nameof(ComponentRelink), Context.Interaction.User.Id); + + EmbedBuilder eb = new(); + eb.WithTitle("Relink"); + eb.WithColor(Color.Blue); + eb.WithDescription("Use this in case you already have a registered Lightless account, but lost access to your previous Discord account." + Environment.NewLine + Environment.NewLine + + "- Have your original registered Lodestone URL ready (i.e. https://eu.finalfantasyxiv.com/lodestone/character/XXXXXXXXX)" + Environment.NewLine + + " - The relink process requires you to modify your Lodestone profile with a generated code for verification" + Environment.NewLine + + "- Do not use this on mobile because you will need to be able to copy the generated secret key"); + ComponentBuilder cb = new(); + AddHome(cb); + cb.WithButton("Start Relink", "wizard-relink-start", ButtonStyle.Primary, emote: new Emoji("🔗")); + await ModifyInteraction(eb, cb).ConfigureAwait(false); + } + + [ComponentInteraction("wizard-relink-start")] + public async Task ComponentRelinkStart() + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}", nameof(ComponentRelinkStart), Context.Interaction.User.Id); + + using var db = await GetDbContext().ConfigureAwait(false); + db.LodeStoneAuth.RemoveRange(db.LodeStoneAuth.Where(u => u.DiscordId == Context.User.Id)); + _botServices.DiscordVerifiedUsers.TryRemove(Context.User.Id, out _); + _botServices.DiscordRelinkLodestoneMapping.TryRemove(Context.User.Id, out _); + await db.SaveChangesAsync().ConfigureAwait(false); + + await RespondWithModalAsync("wizard-relink-lodestone-modal").ConfigureAwait(false); + } + + [ModalInteraction("wizard-relink-lodestone-modal")] + public async Task ModalRelink(LodestoneModal lodestoneModal) + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}:{url}", nameof(ModalRelink), Context.Interaction.User.Id, lodestoneModal.LodestoneUrl); + + EmbedBuilder eb = new(); + eb.WithColor(Color.Purple); + var result = await HandleRelinkModalAsync(eb, lodestoneModal).ConfigureAwait(false); + ComponentBuilder cb = new(); + cb.WithButton("Cancel", "wizard-relink", ButtonStyle.Secondary, emote: new Emoji("❌")); + if (result.Success) cb.WithButton("Verify", "wizard-relink-verify:" + result.LodestoneAuth + "," + result.UID, ButtonStyle.Primary, emote: new Emoji("✅")); + else cb.WithButton("Try again", "wizard-relink-start", ButtonStyle.Primary, emote: new Emoji("🔁")); + await ModifyModalInteraction(eb, cb).ConfigureAwait(false); + } + + [ComponentInteraction("wizard-relink-verify:*,*")] + public async Task ComponentRelinkVerify(string verificationCode, string uid) + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}:{uid}:{verificationCode}", nameof(ComponentRelinkVerify), Context.Interaction.User.Id, uid, verificationCode); + + + _botServices.VerificationQueue.Enqueue(new KeyValuePair>(Context.User.Id, + (services) => HandleVerifyRelinkAsync(Context.User.Id, verificationCode, services))); + EmbedBuilder eb = new(); + ComponentBuilder cb = new(); + eb.WithColor(Color.Purple); + cb.WithButton("Cancel", "wizard-relink", ButtonStyle.Secondary, emote: new Emoji("❌")); + cb.WithButton("Check", "wizard-relink-verify-check:" + verificationCode + "," + uid, ButtonStyle.Primary, emote: new Emoji("❓")); + eb.WithTitle("Relink Verification Pending"); + eb.WithDescription("Please wait until the bot verifies your registration." + Environment.NewLine + + "Press \"Check\" to check if the verification has been already processed" + Environment.NewLine + Environment.NewLine + + "__This will not advance automatically, you need to press \"Check\".__"); + await ModifyInteraction(eb, cb).ConfigureAwait(false); + } + + [ComponentInteraction("wizard-relink-verify-check:*,*")] + public async Task ComponentRelinkVerifyCheck(string verificationCode, string uid) + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}:{uid}:{verificationCode}", nameof(ComponentRelinkVerifyCheck), Context.Interaction.User.Id, uid, verificationCode); + + EmbedBuilder eb = new(); + ComponentBuilder cb = new(); + bool stillEnqueued = _botServices.VerificationQueue.Any(k => k.Key == Context.User.Id); + bool verificationRan = _botServices.DiscordVerifiedUsers.TryGetValue(Context.User.Id, out bool verified); + bool relinkSuccess = false; + if (!verificationRan) + { + if (stillEnqueued) + { + eb.WithColor(Color.Gold); + eb.WithTitle("Your relink verification is still pending"); + eb.WithDescription("Please try again and click Check in a few seconds"); + cb.WithButton("Cancel", "wizard-relink", ButtonStyle.Secondary, emote: new Emoji("❌")); + cb.WithButton("Check", "wizard-relink-verify-check:" + verificationCode + "," + uid, ButtonStyle.Primary, emote: new Emoji("❓")); + } + else + { + eb.WithColor(Color.Red); + eb.WithTitle("Something went wrong"); + eb.WithDescription("Your relink verification was processed but did not arrive properly. Please try to start the relink process from the start."); + cb.WithButton("Restart", "wizard-relink", ButtonStyle.Primary, emote: new Emoji("🔁")); + } + } + else + { + if (verified) + { + eb.WithColor(Color.Green); + using var db = await GetDbContext().ConfigureAwait(false); + var (_, key) = await HandleRelinkUser(db, uid).ConfigureAwait(false); + eb.WithTitle($"Relink successful, your UID is again: {uid}"); + 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 + + $"||**`{key}`**||" + + Environment.NewLine + Environment.NewLine + + "Enter this key in Lightless Sync and hit save to connect to the service." + + Environment.NewLine + Environment.NewLine + + "NOTE: If you are using OAuth2, you do not require to use this secret key." + + Environment.NewLine + + "Have fun."); + AddHome(cb); + + relinkSuccess = true; + } + else + { + eb.WithColor(Color.Gold); + eb.WithTitle("Failed to verify relink"); + eb.WithDescription("The bot was not able to find the required verification code on your Lodestone profile." + Environment.NewLine + Environment.NewLine + + "Please restart your relink process, make sure to save your profile _twice_ for it to be properly saved." + Environment.NewLine + Environment.NewLine + + "**Make sure your profile is set to public (All Users) for your character. The bot cannot read profiles with privacy settings set to \"logged in\" or \"private\".**" + Environment.NewLine + Environment.NewLine + + "The code the bot is looking for is" + Environment.NewLine + Environment.NewLine + + "**`" + verificationCode + "`**"); + cb.WithButton("Cancel", "wizard-relink", emote: new Emoji("❌")); + cb.WithButton("Retry", "wizard-relink-verify:" + verificationCode + "," + uid, ButtonStyle.Primary, emote: new Emoji("🔁")); + } + } + + await ModifyInteraction(eb, cb).ConfigureAwait(false); + if (relinkSuccess) + await _botServices.AddRegisteredRoleAsync(Context.Interaction.User).ConfigureAwait(false); + } + + private async Task<(bool Success, string LodestoneAuth, string UID)> HandleRelinkModalAsync(EmbedBuilder embed, LodestoneModal arg) + { + ulong userId = Context.User.Id; + + 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/"); + return (false, string.Empty, string.Empty); + } + // check if userid is already in db + var hashedLodestoneId = StringUtils.Sha256String(lodestoneId.ToString()); + + using var db = await GetDbContext().ConfigureAwait(false); + + // check if discord id or lodestone id is banned + if (db.BannedRegistrations.Any(a => a.DiscordIdOrLodestoneAuth == hashedLodestoneId)) + { + embed.WithTitle("Illegal operation"); + embed.WithDescription("Your account is banned"); + return (false, string.Empty, string.Empty); + } + + if (!db.LodeStoneAuth.Any(a => a.HashedLodestoneId == hashedLodestoneId)) + { + // character already in db + embed.WithTitle("Impossible operation"); + embed.WithDescription("This lodestone character does not exist in the database."); + return (false, string.Empty, string.Empty); + } + + var expectedUser = await db.LodeStoneAuth.Include(u => u.User).SingleAsync(u => u.HashedLodestoneId == hashedLodestoneId).ConfigureAwait(false); + + string lodestoneAuth = await GenerateLodestoneAuth(Context.User.Id, 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 LIGHTLESS !**" + + 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 relink will be invalidated and you have to relink again."); + _botServices.DiscordRelinkLodestoneMapping[Context.User.Id] = lodestoneId.ToString(); + + return (true, lodestoneAuth, expectedUser.User.UID); + } + + private async Task HandleVerifyRelinkAsync(ulong userid, string authString, DiscordBotServices services) + { + using var req = new HttpClient(); + + services.DiscordVerifiedUsers.Remove(userid, out _); + if (services.DiscordRelinkLodestoneMapping.ContainsKey(userid)) + { + var randomServer = services.LodestoneServers[random.Next(services.LodestoneServers.Length)]; + var url = $"https://{randomServer}.finalfantasyxiv.com/lodestone/character/{services.DiscordRelinkLodestoneMapping[userid]}"; + _logger.LogInformation("Verifying {userid} with URL {url}", userid, url); + using var response = await req.GetAsync(url).ConfigureAwait(false); + if (response.IsSuccessStatusCode || response.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + if (content.Contains(authString)) + { + services.DiscordVerifiedUsers[userid] = true; + _logger.LogInformation("Relink: Verified {userid} from lodestone {lodestone}", userid, services.DiscordRelinkLodestoneMapping[userid]); + await _botServices.LogToChannel($"<@{userid}> RELINK VERIFY: Success.").ConfigureAwait(false); + services.DiscordRelinkLodestoneMapping.TryRemove(userid, out _); + } + else + { + services.DiscordVerifiedUsers[userid] = false; + _logger.LogInformation("Relink: Could not verify {userid} from lodestone {lodestone}, did not find authString: {authString}, status code was: {code}", + userid, services.DiscordRelinkLodestoneMapping[userid], authString, response.StatusCode); + await _botServices.LogToChannel($"<@{userid}> RELINK VERIFY: Failed: No Authstring ({authString}). (<{url}>)").ConfigureAwait(false); + } + } + else + { + _logger.LogWarning("Could not verify {userid}, HttpStatusCode: {code}", userid, response.StatusCode); + await _botServices.LogToChannel($"<@{userid}> RELINK VERIFY: Failed: HttpStatusCode {response.StatusCode}. (<{url}>)").ConfigureAwait(false); + } + } + } + + private async Task<(string, string)> HandleRelinkUser(LightlessDbContext db, string uid) + { + var oldLodestoneAuth = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.User.UID == uid && u.DiscordId != Context.User.Id).ConfigureAwait(false); + var newLodestoneAuth = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.DiscordId == Context.User.Id).ConfigureAwait(false); + + var user = oldLodestoneAuth.User; + + var computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString()); + var auth = new Auth() + { + HashedKey = StringUtils.Sha256String(computedHash), + User = user, + }; + + var previousAuth = await db.Auth.SingleOrDefaultAsync(u => u.UserUID == user.UID).ConfigureAwait(false); + if (previousAuth != null) + { + db.Remove(previousAuth); + } + + newLodestoneAuth.LodestoneAuthString = null; + newLodestoneAuth.StartedAt = null; + newLodestoneAuth.User = user; + db.Update(newLodestoneAuth); + db.Remove(oldLodestoneAuth); + await db.Auth.AddAsync(auth).ConfigureAwait(false); + + _botServices.Logger.LogInformation("User relinked: {userUID}", user.UID); + + await db.SaveChangesAsync().ConfigureAwait(false); + + await _botServices.LogToChannel($"{Context.User.Mention} RELINK COMPLETE: => {user.UID}").ConfigureAwait(false); + + return (user.UID, computedHash); + } +} diff --git a/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Secondary.cs b/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Secondary.cs new file mode 100644 index 0000000..fff7ceb --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Secondary.cs @@ -0,0 +1,91 @@ +using Discord.Interactions; +using Discord; +using LightlessSyncShared.Data; +using Microsoft.EntityFrameworkCore; +using LightlessSyncShared.Models; +using LightlessSyncShared.Utils; + +namespace LightlessSyncServices.Discord; + +public partial class LightlessWizardModule +{ + [ComponentInteraction("wizard-secondary")] + public async Task ComponentSecondary() + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}", nameof(ComponentSecondary), Context.Interaction.User.Id); + + using var lightlessDb = await GetDbContext().ConfigureAwait(false); + var primaryUID = (await lightlessDb.LodeStoneAuth.Include(u => u.User).SingleAsync(u => u.DiscordId == Context.User.Id).ConfigureAwait(false)).User.UID; + var secondaryUids = await lightlessDb.Auth.CountAsync(p => p.PrimaryUserUID == primaryUID).ConfigureAwait(false); + EmbedBuilder eb = new(); + eb.WithColor(Color.Blue); + eb.WithTitle("Secondary UID"); + eb.WithDescription("You can create secondary UIDs here. " + Environment.NewLine + Environment.NewLine + + "Secondary UIDs act as completely separate Lightless accounts with their own pair list, joined syncshells, UID and so on." + Environment.NewLine + + "Use this to create UIDs if you want to use Lightless on two separate game instances at once or keep your alts private." + Environment.NewLine + Environment.NewLine + + "__Note:__ Creating a Secondary UID is _not_ necessary to use Lightless for alts." + Environment.NewLine + Environment.NewLine + + $"You currently have {secondaryUids} Secondary UIDs out of a maximum of 20."); + ComponentBuilder cb = new(); + AddHome(cb); + cb.WithButton("Create Secondary UID", "wizard-secondary-create:" + primaryUID, ButtonStyle.Primary, emote: new Emoji("2️⃣"), disabled: secondaryUids >= 20); + await ModifyInteraction(eb, cb).ConfigureAwait(false); + } + + [ComponentInteraction("wizard-secondary-create:*")] + public async Task ComponentSecondaryCreate(string primaryUid) + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}:{primary}", nameof(ComponentSecondaryCreate), Context.Interaction.User.Id, primaryUid); + + using var lightlessDb = await GetDbContext().ConfigureAwait(false); + EmbedBuilder eb = new(); + eb.WithTitle("Secondary UID created"); + eb.WithColor(Color.Green); + ComponentBuilder cb = new(); + AddHome(cb); + await HandleAddSecondary(lightlessDb, eb, primaryUid).ConfigureAwait(false); + await ModifyInteraction(eb, cb).ConfigureAwait(false); + } + + public async Task HandleAddSecondary(LightlessDbContext db, EmbedBuilder embed, string primaryUID) + { + User newUser = new() + { + IsAdmin = false, + IsModerator = false, + LastLoggedIn = DateTime.UtcNow, + }; + + var hasValidUid = false; + while (!hasValidUid) + { + var uid = StringUtils.GenerateRandomString(10); + 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 = primaryUID + }; + + await db.Users.AddAsync(newUser).ConfigureAwait(false); + await db.Auth.AddAsync(auth).ConfigureAwait(false); + + await db.SaveChangesAsync().ConfigureAwait(false); + + embed.WithDescription("A secondary UID for you was created, use the information below and add the secret key to the Lightless setings in the Service Settings tab."); + embed.AddField("UID", newUser.UID); + embed.AddField("Secret Key", computedHash); + + await _botServices.LogToChannel($"{Context.User.Mention} SECONDARY SUCCESS: {newUser.UID}").ConfigureAwait(false); + } + +} diff --git a/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.UserInfo.cs b/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.UserInfo.cs new file mode 100644 index 0000000..c243842 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.UserInfo.cs @@ -0,0 +1,80 @@ +using Discord.Interactions; +using Discord; +using LightlessSyncShared.Data; +using Microsoft.EntityFrameworkCore; + +namespace LightlessSyncServices.Discord; + +public partial class LightlessWizardModule +{ + [ComponentInteraction("wizard-userinfo")] + public async Task ComponentUserinfo() + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}", nameof(ComponentUserinfo), Context.Interaction.User.Id); + + using var lightlessDb = await GetDbContext().ConfigureAwait(false); + EmbedBuilder eb = new(); + eb.WithTitle("User Info"); + eb.WithColor(Color.Blue); + eb.WithDescription("You can see information about your user account(s) here." + Environment.NewLine + + "Use the selection below to select a user account to see info for." + Environment.NewLine + Environment.NewLine + + "- 1️⃣ is your primary account/UID" + Environment.NewLine + + "- 2️⃣ are all your secondary accounts/UIDs" + Environment.NewLine + + "If you are using Vanity UIDs the original UID is displayed in the second line of the account selection."); + ComponentBuilder cb = new(); + await AddUserSelection(lightlessDb, cb, "wizard-userinfo-select").ConfigureAwait(false); + AddHome(cb); + await ModifyInteraction(eb, cb).ConfigureAwait(false); + } + + [ComponentInteraction("wizard-userinfo-select")] + public async Task SelectionUserinfo(string uid) + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}:{uid}", nameof(SelectionUserinfo), Context.Interaction.User.Id, uid); + + using var lightlessDb = await GetDbContext().ConfigureAwait(false); + EmbedBuilder eb = new(); + eb.WithTitle($"User Info for {uid}"); + await HandleUserInfo(eb, lightlessDb, uid).ConfigureAwait(false); + eb.WithColor(Color.Green); + ComponentBuilder cb = new(); + await AddUserSelection(lightlessDb, cb, "wizard-userinfo-select").ConfigureAwait(false); + AddHome(cb); + await ModifyInteraction(eb, cb).ConfigureAwait(false); + } + + private async Task HandleUserInfo(EmbedBuilder eb, LightlessDbContext db, string uid) + { + ulong userToCheckForDiscordId = Context.User.Id; + + var dbUser = await db.Users.SingleOrDefaultAsync(u => u.UID == 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.WithDescription("This is the user info for your selected UID. You can check other UIDs or go back using the menu below."); + if (!string.IsNullOrEmpty(dbUser.Alias)) + { + eb.AddField("Vanity UID", dbUser.Alias); + } + eb.AddField("Last Online (UTC)", dbUser.LastLoggedIn.ToString("U")); + eb.AddField("Currently online ", !string.IsNullOrEmpty(identity)); + 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); + } + } + +} diff --git a/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Vanity.cs b/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Vanity.cs new file mode 100644 index 0000000..c926bc4 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Vanity.cs @@ -0,0 +1,205 @@ +using Discord.Interactions; +using Discord; +using Microsoft.EntityFrameworkCore; +using System.Text.RegularExpressions; +using System.Text; + +namespace LightlessSyncServices.Discord; + +public partial class LightlessWizardModule +{ + [ComponentInteraction("wizard-vanity")] + public async Task ComponentVanity() + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}", nameof(ComponentVanity), Context.Interaction.User.Id); + + StringBuilder sb = new(); + var user = await Context.Guild.GetUserAsync(Context.User.Id).ConfigureAwait(false); + bool userIsInVanityRole = _botServices.VanityRoles.Keys.Any(u => user.RoleIds.Contains(u.Id)) || !_botServices.VanityRoles.Any(); + if (!userIsInVanityRole) + { + sb.AppendLine("To be able to set Vanity IDs you must have one of the following roles:"); + foreach (var role in _botServices.VanityRoles) + { + sb.Append("- ").Append(role.Key.Mention).Append(" (").Append(role.Value).AppendLine(")"); + } + } + else + { + sb.AppendLine("Your current roles on this server allow you to set Vanity IDs."); + } + + EmbedBuilder eb = new(); + eb.WithTitle("Vanity IDs"); + eb.WithDescription("You are able to set your Vanity IDs here." + Environment.NewLine + + "Vanity IDs are a way to customize your displayed UID or Syncshell ID to others." + Environment.NewLine + Environment.NewLine + + sb.ToString()); + eb.WithColor(Color.Blue); + ComponentBuilder cb = new(); + AddHome(cb); + if (userIsInVanityRole) + { + using var db = await GetDbContext().ConfigureAwait(false); + await AddUserSelection(db, cb, "wizard-vanity-uid").ConfigureAwait(false); + await AddGroupSelection(db, cb, "wizard-vanity-gid").ConfigureAwait(false); + } + + await ModifyInteraction(eb, cb).ConfigureAwait(false); + } + + [ComponentInteraction("wizard-vanity-uid")] + public async Task SelectionVanityUid(string uid) + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}:{uid}", nameof(SelectionVanityUid), Context.Interaction.User.Id, uid); + + using var db = await GetDbContext().ConfigureAwait(false); + var user = db.Users.Single(u => u.UID == uid); + EmbedBuilder eb = new(); + eb.WithColor(Color.Purple); + eb.WithTitle($"Set Vanity UID for {uid}"); + eb.WithDescription($"You are about to change the Vanity UID for {uid}" + Environment.NewLine + Environment.NewLine + + "The current Vanity UID is set to: **" + (user.Alias == null ? "No Vanity UID set" : user.Alias) + "**"); + ComponentBuilder cb = new(); + cb.WithButton("Cancel", "wizard-vanity", ButtonStyle.Secondary, emote: new Emoji("❌")); + cb.WithButton("Set Vanity ID", "wizard-vanity-uid-set:" + uid, ButtonStyle.Primary, new Emoji("💅")); + + await ModifyInteraction(eb, cb).ConfigureAwait(false); + } + + [ComponentInteraction("wizard-vanity-uid-set:*")] + public async Task SelectionVanityUidSet(string uid) + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}:{uid}", nameof(SelectionVanityUidSet), Context.Interaction.User.Id, uid); + + await RespondWithModalAsync("wizard-vanity-uid-modal:" + uid).ConfigureAwait(false); + } + + [ModalInteraction("wizard-vanity-uid-modal:*")] + public async Task ConfirmVanityUidModal(string uid, VanityUidModal modal) + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}:{uid}:{vanity}", nameof(ConfirmVanityUidModal), Context.Interaction.User.Id, uid, modal.DesiredVanityUID); + + EmbedBuilder eb = new(); + ComponentBuilder cb = new(); + var desiredVanityUid = modal.DesiredVanityUID; + using var db = await GetDbContext().ConfigureAwait(false); + bool canAddVanityId = !db.Users.Any(u => u.UID == modal.DesiredVanityUID || u.Alias == modal.DesiredVanityUID); + + Regex rgx = new(@"^[_\-a-zA-Z0-9]{5,15}$", RegexOptions.ECMAScript); + if (!rgx.Match(desiredVanityUid).Success) + { + eb.WithColor(Color.Red); + eb.WithTitle("Invalid Vanity UID"); + eb.WithDescription("A Vanity UID must be between 5 and 15 characters long and only contain the letters A-Z, numbers 0-9, dashes (-) and underscores (_)."); + cb.WithButton("Cancel", "wizard-vanity", ButtonStyle.Secondary, emote: new Emoji("❌")); + cb.WithButton("Pick Different UID", "wizard-vanity-uid-set:" + uid, ButtonStyle.Primary, new Emoji("💅")); + } + else if (!canAddVanityId) + { + eb.WithColor(Color.Red); + eb.WithTitle("Vanity UID already taken"); + eb.WithDescription($"The Vanity UID {desiredVanityUid} has already been claimed. Please pick a different one."); + cb.WithButton("Cancel", "wizard-vanity", ButtonStyle.Secondary, emote: new Emoji("❌")); + cb.WithButton("Pick Different UID", "wizard-vanity-uid-set:" + uid, ButtonStyle.Primary, new Emoji("💅")); + } + else + { + var user = await db.Users.SingleAsync(u => u.UID == uid).ConfigureAwait(false); + user.Alias = desiredVanityUid; + db.Update(user); + await db.SaveChangesAsync().ConfigureAwait(false); + eb.WithColor(Color.Green); + eb.WithTitle("Vanity UID successfully set"); + eb.WithDescription($"Your Vanity UID for \"{uid}\" was successfully changed to \"{desiredVanityUid}\"." + Environment.NewLine + Environment.NewLine + + "For changes to take effect you need to reconnect to the Lightless service."); + await _botServices.LogToChannel($"{Context.User.Mention} VANITY UID SET: UID: {user.UID}, Vanity: {desiredVanityUid}").ConfigureAwait(false); + AddHome(cb); + } + + await ModifyModalInteraction(eb, cb).ConfigureAwait(false); + } + + [ComponentInteraction("wizard-vanity-gid")] + public async Task SelectionVanityGid(string gid) + { + _logger.LogInformation("{method}:{userId}:{uid}", nameof(SelectionVanityGid), Context.Interaction.User.Id, gid); + + using var db = await GetDbContext().ConfigureAwait(false); + var group = db.Groups.Single(u => u.GID == gid); + EmbedBuilder eb = new(); + eb.WithColor(Color.Purple); + eb.WithTitle($"Set Vanity GID for {gid}"); + eb.WithDescription($"You are about to change the Vanity Syncshell ID for {gid}" + Environment.NewLine + Environment.NewLine + + "The current Vanity Syncshell ID is set to: **" + (group.Alias == null ? "No Vanity Syncshell ID set" : group.Alias) + "**"); + ComponentBuilder cb = new(); + cb.WithButton("Cancel", "wizard-vanity", ButtonStyle.Secondary, emote: new Emoji("❌")); + cb.WithButton("Set Vanity ID", "wizard-vanity-gid-set:" + gid, ButtonStyle.Primary, new Emoji("💅")); + + await ModifyInteraction(eb, cb).ConfigureAwait(false); + } + + [ComponentInteraction("wizard-vanity-gid-set:*")] + public async Task SelectionVanityGidSet(string gid) + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}:{gid}", nameof(SelectionVanityGidSet), Context.Interaction.User.Id, gid); + + await RespondWithModalAsync("wizard-vanity-gid-modal:" + gid).ConfigureAwait(false); + } + + [ModalInteraction("wizard-vanity-gid-modal:*")] + public async Task ConfirmVanityGidModal(string gid, VanityGidModal modal) + { + if (!(await ValidateInteraction().ConfigureAwait(false))) return; + + _logger.LogInformation("{method}:{userId}:{gid}:{vanity}", nameof(ConfirmVanityGidModal), Context.Interaction.User.Id, gid, modal.DesiredVanityGID); + + EmbedBuilder eb = new(); + ComponentBuilder cb = new(); + var desiredVanityGid = modal.DesiredVanityGID; + using var db = await GetDbContext().ConfigureAwait(false); + bool canAddVanityId = !db.Groups.Any(u => u.GID == modal.DesiredVanityGID || u.Alias == modal.DesiredVanityGID); + + Regex rgx = new(@"^[_\-a-zA-Z0-9]{5,20}$", RegexOptions.ECMAScript); + if (!rgx.Match(desiredVanityGid).Success) + { + eb.WithColor(Color.Red); + eb.WithTitle("Invalid Vanity Syncshell ID"); + eb.WithDescription("A Vanity Syncshell ID must be between 5 and 20 characters long and only contain the letters A-Z, numbers 0-9, dashes (-) and underscores (_)."); + cb.WithButton("Cancel", "wizard-vanity", ButtonStyle.Secondary, emote: new Emoji("❌")); + cb.WithButton("Pick Different ID", "wizard-vanity-gid-set:" + gid, ButtonStyle.Primary, new Emoji("💅")); + } + else if (!canAddVanityId) + { + eb.WithColor(Color.Red); + eb.WithTitle("Vanity Syncshell ID already taken"); + eb.WithDescription($"The Vanity Synshell ID \"{desiredVanityGid}\" has already been claimed. Please pick a different one."); + cb.WithButton("Cancel", "wizard-vanity", ButtonStyle.Secondary, emote: new Emoji("❌")); + cb.WithButton("Pick Different ID", "wizard-vanity-gid-set:" + gid, ButtonStyle.Primary, new Emoji("💅")); + } + else + { + var group = await db.Groups.SingleAsync(u => u.GID == gid).ConfigureAwait(false); + group.Alias = desiredVanityGid; + db.Update(group); + await db.SaveChangesAsync().ConfigureAwait(false); + eb.WithColor(Color.Green); + eb.WithTitle("Vanity Syncshell ID successfully set"); + eb.WithDescription($"Your Vanity Syncshell ID for {gid} was successfully changed to \"{desiredVanityGid}\"." + Environment.NewLine + Environment.NewLine + + "For changes to take effect you need to reconnect to the Lightless service."); + AddHome(cb); + await _botServices.LogToChannel($"{Context.User.Mention} VANITY GID SET: GID: {group.GID}, Vanity: {desiredVanityGid}").ConfigureAwait(false); + } + + await ModifyModalInteraction(eb, cb).ConfigureAwait(false); + } +} diff --git a/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.cs b/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.cs new file mode 100644 index 0000000..b474ee6 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.cs @@ -0,0 +1,346 @@ +using Discord; +using Discord.Interactions; +using Discord.WebSocket; +using LightlessSyncShared.Data; +using LightlessSyncShared.Models; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils; +using LightlessSyncShared.Utils.Configuration; +using Microsoft.EntityFrameworkCore; +using StackExchange.Redis; +using System.Text.RegularExpressions; + +namespace LightlessSyncServices.Discord; + +public partial class LightlessWizardModule : InteractionModuleBase +{ + private ILogger _logger; + private DiscordBotServices _botServices; + private IConfigurationService _lightlessClientConfigurationService; + private IConfigurationService _lightlessServicesConfiguration; + private IConnectionMultiplexer _connectionMultiplexer; + private readonly IDbContextFactory _dbContextFactory; + private Random random = new(); + + public LightlessWizardModule(ILogger logger, DiscordBotServices botServices, + IConfigurationService lightlessClientConfigurationService, + IConfigurationService lightlessServicesConfiguration, + IConnectionMultiplexer connectionMultiplexer, IDbContextFactory dbContextFactory) + { + _logger = logger; + _botServices = botServices; + _lightlessClientConfigurationService = lightlessClientConfigurationService; + _lightlessServicesConfiguration = lightlessServicesConfiguration; + _connectionMultiplexer = connectionMultiplexer; + _dbContextFactory = dbContextFactory; + } + + [ComponentInteraction("wizard-captcha:*")] + public async Task WizardCaptcha(bool init = false) + { + if (!init && !(await ValidateInteraction().ConfigureAwait(false))) return; + + if (_botServices.VerifiedCaptchaUsers.Contains(Context.Interaction.User.Id)) + { + await StartWizard(true).ConfigureAwait(false); + return; + } + + EmbedBuilder eb = new(); + + Random rnd = new Random(); + var correctButton = rnd.Next(4) + 1; + string nthButtonText = correctButton switch + { + 1 => "first", + 2 => "second", + 3 => "third", + 4 => "fourth", + _ => "unknown", + }; + + Emoji nthButtonEmoji = correctButton switch + { + 1 => new Emoji("⬅️"), + 2 => new Emoji("🤖"), + 3 => new Emoji("‼️"), + 4 => new Emoji("✉️"), + _ => "unknown", + }; + + eb.WithTitle("Lightless Bot Services Captcha"); + eb.WithDescription("You are seeing this embed because you interact with this bot for the first time since the bot has been restarted." + Environment.NewLine + Environment.NewLine + + "This bot __requires__ embeds for its function. To proceed, please verify you have embeds enabled." + Environment.NewLine + + $"## To verify you have embeds enabled __press on the **{nthButtonText}** button ({nthButtonEmoji}).__"); + eb.WithColor(Color.LightOrange); + + int incorrectButtonHighlight = 1; + do + { + incorrectButtonHighlight = rnd.Next(4) + 1; + } + while (incorrectButtonHighlight == correctButton); + + ComponentBuilder cb = new(); + cb.WithButton("This", correctButton == 1 ? "wizard-home:false" : "wizard-captcha-fail:1", emote: new Emoji("⬅️"), style: incorrectButtonHighlight == 1 ? ButtonStyle.Primary : ButtonStyle.Secondary); + cb.WithButton("Bot", correctButton == 2 ? "wizard-home:false" : "wizard-captcha-fail:2", emote: new Emoji("🤖"), style: incorrectButtonHighlight == 2 ? ButtonStyle.Primary : ButtonStyle.Secondary); + cb.WithButton("Requires", correctButton == 3 ? "wizard-home:false" : "wizard-captcha-fail:3", emote: new Emoji("‼️"), style: incorrectButtonHighlight == 3 ? ButtonStyle.Primary : ButtonStyle.Secondary); + cb.WithButton("Embeds", correctButton == 4 ? "wizard-home:false" : "wizard-captcha-fail:4", emote: new Emoji("✉️"), style: incorrectButtonHighlight == 4 ? ButtonStyle.Primary : ButtonStyle.Secondary); + + await InitOrUpdateInteraction(init, eb, cb).ConfigureAwait(false); + } + + private async Task InitOrUpdateInteraction(bool init, EmbedBuilder eb, ComponentBuilder cb) + { + if (init) + { + await RespondAsync(embed: eb.Build(), components: cb.Build(), ephemeral: true).ConfigureAwait(false); + var resp = await GetOriginalResponseAsync().ConfigureAwait(false); + _botServices.ValidInteractions[Context.User.Id] = resp.Id; + _logger.LogInformation("Init Msg: {id}", resp.Id); + } + else + { + await ModifyInteraction(eb, cb).ConfigureAwait(false); + } + } + + [ComponentInteraction("wizard-captcha-fail:*")] + public async Task WizardCaptchaFail(int button) + { + ComponentBuilder cb = new(); + cb.WithButton("Restart (with Embeds enabled)", "wizard-captcha:false", emote: new Emoji("↩️")); + await ((Context.Interaction) as IComponentInteraction).UpdateAsync(m => + { + m.Embed = null; + m.Content = "You pressed the wrong button. You likely have embeds disabled. Enable embeds in your Discord client (Settings -> Chat -> \"Show embeds and preview website links pasted into chat\") and try again."; + m.Components = cb.Build(); + }).ConfigureAwait(false); + + await _botServices.LogToChannel($"{Context.User.Mention} FAILED CAPTCHA").ConfigureAwait(false); + } + + + [ComponentInteraction("wizard-home:*")] + public async Task StartWizard(bool init = false) + { + if (!init && !(await ValidateInteraction().ConfigureAwait(false))) return; + + if (!_botServices.VerifiedCaptchaUsers.Contains(Context.Interaction.User.Id)) + _botServices.VerifiedCaptchaUsers.Add(Context.Interaction.User.Id); + + _logger.LogInformation("{method}:{userId}", nameof(StartWizard), Context.Interaction.User.Id); + + using var lightlessDb = await GetDbContext().ConfigureAwait(false); + bool hasAccount = await lightlessDb.LodeStoneAuth.AnyAsync(u => u.DiscordId == Context.User.Id && u.StartedAt == null).ConfigureAwait(false); + + if (init) + { + bool isBanned = await lightlessDb.BannedRegistrations.AnyAsync(u => u.DiscordIdOrLodestoneAuth == Context.User.Id.ToString()).ConfigureAwait(false); + + if (isBanned) + { + EmbedBuilder ebBanned = new(); + ebBanned.WithTitle("You are not welcome here"); + ebBanned.WithDescription("Your Discord account is banned"); + await RespondAsync(embed: ebBanned.Build(), ephemeral: true).ConfigureAwait(false); + return; + } + } +#if !DEBUG + bool isInAprilFoolsMode = _lightlessServicesConfiguration.GetValueOrDefault(nameof(ServicesConfiguration.DiscordRoleAprilFools2024), null) != null + && DateTime.UtcNow.Month == 4 && DateTime.UtcNow.Day == 1 && DateTime.UtcNow.Year == 2024 && DateTime.UtcNow.Hour >= 10; +#elif DEBUG + bool isInAprilFoolsMode = true; +#endif + + EmbedBuilder eb = new(); + eb.WithTitle("Welcome to the Lightless Sync Service Bot for this server"); + eb.WithDescription("Here is what you can do:" + Environment.NewLine + Environment.NewLine + + (!hasAccount ? string.Empty : ("- Check your account status press \"ℹ️ User Info\"" + Environment.NewLine)) + + (hasAccount ? string.Empty : ("- Register a new Lightless Account press \"🌒 Register\"" + Environment.NewLine)) + + (!hasAccount ? string.Empty : ("- You lost your secret key press \"🏥 Recover\"" + Environment.NewLine)) + + (hasAccount ? string.Empty : ("- If you have changed your Discord account press \"🔗 Relink\"" + Environment.NewLine)) + + (!hasAccount ? string.Empty : ("- Create a secondary UIDs press \"2️⃣ Secondary UID\"" + Environment.NewLine)) + + (!hasAccount ? string.Empty : ("- Set a Vanity UID press \"💅 Vanity IDs\"" + Environment.NewLine)) + + (!hasAccount ? string.Empty : (!isInAprilFoolsMode ? string.Empty : ("- Check your WorryCoin™ and LightlessToken© balance and add payment options" + Environment.NewLine))) + + (!hasAccount ? string.Empty : ("- Delete your primary or secondary accounts with \"⚠️ Delete\"")) + ); + eb.WithColor(Color.Blue); + ComponentBuilder cb = new(); + if (!hasAccount) + { + cb.WithButton("Register", "wizard-register", ButtonStyle.Primary, new Emoji("🌒")); + cb.WithButton("Relink", "wizard-relink", ButtonStyle.Secondary, new Emoji("🔗")); + } + else + { + cb.WithButton("User Info", "wizard-userinfo", ButtonStyle.Secondary, new Emoji("ℹ️")); + cb.WithButton("Recover", "wizard-recover", ButtonStyle.Secondary, new Emoji("🏥")); + cb.WithButton("Secondary UID", "wizard-secondary", ButtonStyle.Secondary, new Emoji("2️⃣")); + cb.WithButton("Vanity IDs", "wizard-vanity", ButtonStyle.Secondary, new Emoji("💅")); + if (isInAprilFoolsMode) + { + cb.WithButton("WorryCoin™ and LightlessToken© management", "wizard-fools", ButtonStyle.Primary, new Emoji("💲")); + } + cb.WithButton("Delete", "wizard-delete", ButtonStyle.Danger, new Emoji("⚠️")); + } + + await InitOrUpdateInteraction(init, eb, cb).ConfigureAwait(false); + } + + public class VanityUidModal : IModal + { + public string Title => "Set Vanity UID"; + + [InputLabel("Set your Vanity UID")] + [ModalTextInput("vanity_uid", TextInputStyle.Short, "5-15 characters, underscore, dash", 5, 15)] + public string DesiredVanityUID { get; set; } + } + + public class VanityGidModal : IModal + { + public string Title => "Set Vanity Syncshell ID"; + + [InputLabel("Set your Vanity Syncshell ID")] + [ModalTextInput("vanity_gid", TextInputStyle.Short, "5-20 characters, underscore, dash", 5, 20)] + public string DesiredVanityGID { get; set; } + } + + public class ConfirmDeletionModal : IModal + { + public string Title => "Confirm Account Deletion"; + + [InputLabel("Enter \"DELETE\" in all Caps")] + [ModalTextInput("confirmation", TextInputStyle.Short, "Enter DELETE")] + public string Delete { get; set; } + } + + private async Task GetDbContext() + { + return await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false); + } + + private async Task ValidateInteraction() + { + if (Context.Interaction is not IComponentInteraction componentInteraction) return true; + + if (_botServices.ValidInteractions.TryGetValue(Context.User.Id, out ulong interactionId) && interactionId == componentInteraction.Message.Id) + { + return true; + } + + EmbedBuilder eb = new(); + eb.WithTitle("Session expired"); + eb.WithDescription("This session has expired since you have either again pressed \"Start\" on the initial message or the bot has been restarted." + Environment.NewLine + Environment.NewLine + + "Please use the newly started interaction or start a new one."); + eb.WithColor(Color.Red); + ComponentBuilder cb = new(); + await ModifyInteraction(eb, cb).ConfigureAwait(false); + + return false; + } + + private void AddHome(ComponentBuilder cb) + { + cb.WithButton("Return to Home", "wizard-home:false", ButtonStyle.Secondary, new Emoji("🏠")); + } + + private async Task ModifyModalInteraction(EmbedBuilder eb, ComponentBuilder cb) + { + await (Context.Interaction as SocketModal).UpdateAsync(m => + { + m.Embed = eb.Build(); + m.Components = cb.Build(); + }).ConfigureAwait(false); + } + + private async Task ModifyInteraction(EmbedBuilder eb, ComponentBuilder cb) + { + await ((Context.Interaction) as IComponentInteraction).UpdateAsync(m => + { + m.Content = null; + m.Embed = eb.Build(); + m.Components = cb.Build(); + }).ConfigureAwait(false); + } + + private async Task AddUserSelection(LightlessDbContext lightlessDb, ComponentBuilder cb, string customId) + { + var discordId = Context.User.Id; + var existingAuth = await lightlessDb.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(e => e.DiscordId == discordId).ConfigureAwait(false); + if (existingAuth != null) + { + SelectMenuBuilder sb = new(); + sb.WithPlaceholder("Select a UID"); + sb.WithCustomId(customId); + var existingUids = await lightlessDb.Auth.Include(u => u.User).Where(u => u.UserUID == existingAuth.User.UID || u.PrimaryUserUID == existingAuth.User.UID) + .OrderByDescending(u => u.PrimaryUser == null).ToListAsync().ConfigureAwait(false); + foreach (var entry in existingUids) + { + sb.AddOption(string.IsNullOrEmpty(entry.User.Alias) ? entry.UserUID : entry.User.Alias, + entry.UserUID, + !string.IsNullOrEmpty(entry.User.Alias) ? entry.User.UID : null, + entry.PrimaryUserUID == null ? new Emoji("1️⃣") : new Emoji("2️⃣")); + } + cb.WithSelectMenu(sb); + } + } + + private async Task AddGroupSelection(LightlessDbContext db, ComponentBuilder cb, string customId) + { + var primary = (await db.LodeStoneAuth.Include(u => u.User).SingleAsync(u => u.DiscordId == Context.User.Id).ConfigureAwait(false)).User; + var secondary = await db.Auth.Include(u => u.User).Where(u => u.PrimaryUserUID == primary.UID).Select(u => u.User).ToListAsync().ConfigureAwait(false); + var primaryGids = (await db.Groups.Include(u => u.Owner).Where(u => u.OwnerUID == primary.UID).ToListAsync().ConfigureAwait(false)); + var secondaryGids = (await db.Groups.Include(u => u.Owner).Where(u => secondary.Select(u => u.UID).Contains(u.OwnerUID)).ToListAsync().ConfigureAwait(false)); + SelectMenuBuilder gids = new(); + if (primaryGids.Any() || secondaryGids.Any()) + { + foreach (var item in primaryGids) + { + gids.AddOption(item.Alias ?? item.GID, item.GID, (item.Alias == null ? string.Empty : item.GID) + $" ({item.Owner.Alias ?? item.Owner.UID})", new Emoji("1️⃣")); + } + foreach (var item in secondaryGids) + { + gids.AddOption(item.Alias ?? item.GID, item.GID, (item.Alias == null ? string.Empty : item.GID) + $" ({item.Owner.Alias ?? item.Owner.UID})", new Emoji("2️⃣")); + } + gids.WithCustomId(customId); + gids.WithPlaceholder("Select a Syncshell"); + cb.WithSelectMenu(gids); + } + } + + private async Task GenerateLodestoneAuth(ulong discordid, string hashedLodestoneId, LightlessDbContext dbContext) + { + var auth = StringUtils.GenerateRandomString(12, "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz"); + 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; + } +} diff --git a/LightlessSyncServer/LightlessSyncServices/LightlessSyncServices.csproj b/LightlessSyncServer/LightlessSyncServices/LightlessSyncServices.csproj new file mode 100644 index 0000000..3a2057e --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServices/LightlessSyncServices.csproj @@ -0,0 +1,42 @@ + + + + net9.0 + enable + + + + + + + + + + + + + + Never + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/LightlessSyncServer/LightlessSyncServices/Program.cs b/LightlessSyncServer/LightlessSyncServices/Program.cs new file mode 100644 index 0000000..a50e182 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServices/Program.cs @@ -0,0 +1,57 @@ +using LightlessSyncServices; +using LightlessSyncShared.Services; +using LightlessSyncShared.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 options = host.Services.GetService>(); + var optionsServer = host.Services.GetService>(); + var logger = host.Services.GetService>(); + logger.LogInformation("Loaded LightlessSync Services Configuration (IsMain: {isMain})", options.IsMain); + logger.LogInformation(options.ToString()); + logger.LogInformation("Loaded LightlessSync Server Configuration (IsMain: {isMain})", optionsServer.IsMain); + logger.LogInformation(optionsServer.ToString()); + } + + host.Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSystemd() + .UseConsoleLifetime() + .ConfigureAppConfiguration((ctx, config) => + { + var appSettingsPath = Environment.GetEnvironmentVariable("APPSETTINGS_PATH"); + if (!string.IsNullOrEmpty(appSettingsPath)) + { + config.AddJsonFile(appSettingsPath, optional: true, reloadOnChange: true); + } + else + { + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); + } + + config.AddEnvironmentVariables(); + }) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseContentRoot(AppContext.BaseDirectory); + webBuilder.ConfigureLogging((ctx, builder) => + { + builder.AddConfiguration(ctx.Configuration.GetSection("Logging")); + builder.AddFile(o => o.RootPath = AppContext.BaseDirectory); + }); + webBuilder.ConfigureKestrel((opt) => + { + }); + webBuilder.UseStartup(); + }); +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncServices/Properties/launchSettings.json b/LightlessSyncServer/LightlessSyncServices/Properties/launchSettings.json new file mode 100644 index 0000000..e186dcc --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServices/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "LightlessSyncServices": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5294;https://localhost:7294", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/LightlessSyncServer/LightlessSyncServices/Startup.cs b/LightlessSyncServer/LightlessSyncServices/Startup.cs new file mode 100644 index 0000000..f4f60f8 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServices/Startup.cs @@ -0,0 +1,76 @@ +using LightlessSyncServices.Discord; +using LightlessSyncShared.Data; +using LightlessSyncShared.Metrics; +using Microsoft.EntityFrameworkCore; +using Prometheus; +using LightlessSyncShared.Utils; +using LightlessSyncShared.Services; +using StackExchange.Redis; +using LightlessSyncShared.Utils.Configuration; + +namespace LightlessSyncServices; + +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(LightlessConfigurationBase.MetricsPort), 4982)); + metricServer.Start(); + } + + public void ConfigureServices(IServiceCollection services) + { + var lightlessConfig = Configuration.GetSection("LightlessSync"); + + services.AddDbContextPool(options => + { + options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder => + { + builder.MigrationsHistoryTable("_efmigrationshistory", "public"); + }).UseSnakeCaseNamingConvention(); + options.EnableThreadSafetyChecks(false); + }, Configuration.GetValue(nameof(LightlessConfigurationBase.DbContextPoolSize), 1024)); + services.AddDbContextFactory(options => + { + options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder => + { + builder.MigrationsHistoryTable("_efmigrationshistory", "public"); + builder.MigrationsAssembly("LightlessSyncShared"); + }).UseSnakeCaseNamingConvention(); + options.EnableThreadSafetyChecks(false); + }); + + services.AddSingleton(m => new LightlessMetrics(m.GetService>(), new List { }, + new List { })); + + var redis = lightlessConfig.GetValue(nameof(ServerConfiguration.RedisConnectionString), string.Empty); + var options = ConfigurationOptions.Parse(redis); + options.ClientName = "Lightless"; + options.ChannelPrefix = "UserData"; + ConnectionMultiplexer connectionMultiplexer = ConnectionMultiplexer.Connect(options); + services.AddSingleton(connectionMultiplexer); + + services.Configure(Configuration.GetRequiredSection("LightlessSync")); + services.Configure(Configuration.GetRequiredSection("LightlessSync")); + services.Configure(Configuration.GetRequiredSection("LightlessSync")); + services.AddSingleton(Configuration); + services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(); + services.AddSingleton, LightlessConfigurationServiceServer>(); + services.AddSingleton, LightlessConfigurationServiceClient>(); + services.AddSingleton, LightlessConfigurationServiceClient>(); + + services.AddHostedService(p => (LightlessConfigurationServiceClient)p.GetService>()); + services.AddHostedService(p => (LightlessConfigurationServiceClient)p.GetService>()); + } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncServices/appsettings.Development.json b/LightlessSyncServer/LightlessSyncServices/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServices/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/LightlessSyncServer/LightlessSyncServices/appsettings.json b/LightlessSyncServer/LightlessSyncServices/appsettings.json new file mode 100644 index 0000000..afee902 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServices/appsettings.json @@ -0,0 +1,29 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5432;Database=;Username=;Password=" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Kestrel": { + "Endpoints": { + "Http": { + "Protocols": "Http2", + "Url": "http://+:5002" + } + } + }, + "LightlessSync": { + "DbContextPoolSize": 1024, + "DiscordBotToken": "", + "DiscordChannelForMessages": "", + "PurgeUnusedAccounts": true, + "PurgeUnusedAccountsPeriodInDays": 14, + "FailedAuthForTempBan": 5, + "TempBanDurationInMinutes": 30 + }, + "AllowedHosts": "*" +} diff --git a/LightlessSyncServer/LightlessSyncShared/Data/MareDbContext.cs b/LightlessSyncServer/LightlessSyncShared/Data/MareDbContext.cs new file mode 100644 index 0000000..dc7b17f --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Data/MareDbContext.cs @@ -0,0 +1,145 @@ +using LightlessSyncShared.Models; +using Microsoft.EntityFrameworkCore; + +namespace LightlessSyncShared.Data; + +public class LightlessDbContext : DbContext +{ +#if DEBUG + public LightlessDbContext() { } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (optionsBuilder.IsConfigured) + { + base.OnConfiguring(optionsBuilder); + return; + } + + optionsBuilder.UseNpgsql("Host=localhost;Port=5432;Database=;Username=;Password=", builder => + { + builder.MigrationsHistoryTable("_efmigrationshistory", "public"); + builder.MigrationsAssembly("LightlessSyncShared"); + }).UseSnakeCaseNamingConvention(); + optionsBuilder.EnableThreadSafetyChecks(false); + + base.OnConfiguring(optionsBuilder); + } +#endif + + public LightlessDbContext(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 Users { get; set; } + public DbSet Permissions { get; set; } + public DbSet GroupPairPreferredPermissions { get; set; } + public DbSet UserDefaultPreferredPermissions { 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_permission_sets"); + mb.Entity().HasKey(u => new { u.UserUID, u.OtherUserUID }); + mb.Entity().HasIndex(c => c.UserUID); + mb.Entity().HasIndex(c => c.OtherUserUID); + mb.Entity().HasIndex(c => new { c.UserUID, c.OtherUserUID, c.IsPaused }); + mb.Entity().ToTable("group_pair_preferred_permissions"); + mb.Entity().HasKey(u => new { u.UserUID, u.GroupGID }); + mb.Entity().HasIndex(c => c.UserUID); + mb.Entity().HasIndex(c => c.GroupGID); + mb.Entity().ToTable("user_default_preferred_permissions"); + mb.Entity().HasKey(u => u.UserUID); + mb.Entity().HasIndex(u => u.UserUID); + mb.Entity().HasOne(u => u.User); + 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/LightlessSyncServer/LightlessSyncShared/Extensions.cs b/LightlessSyncServer/LightlessSyncShared/Extensions.cs new file mode 100644 index 0000000..92ed4e0 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Extensions.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Http; + +namespace LightlessSyncShared; + +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/LightlessSyncServer/LightlessSyncShared/LightlessSyncShared.csproj b/LightlessSyncServer/LightlessSyncShared/LightlessSyncShared.csproj new file mode 100644 index 0000000..8cc310e --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/LightlessSyncShared.csproj @@ -0,0 +1,57 @@ + + + + net9.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/LightlessSyncServer/LightlessSyncShared/Metrics/MareMetrics.cs b/LightlessSyncServer/LightlessSyncShared/Metrics/MareMetrics.cs new file mode 100644 index 0000000..046f8c0 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Metrics/MareMetrics.cs @@ -0,0 +1,84 @@ +using Microsoft.Extensions.Logging; +using Prometheus; + +namespace LightlessSyncShared.Metrics; + +public class LightlessMetrics +{ + public LightlessMetrics(ILogger logger, List countersToServe, List gaugesToServe) + { + logger.LogInformation("Initializing LightlessMetrics"); + 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/LightlessSyncServer/LightlessSyncShared/Metrics/MetricsAPI.cs b/LightlessSyncServer/LightlessSyncShared/Metrics/MetricsAPI.cs new file mode 100644 index 0000000..f3b09a6 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Metrics/MetricsAPI.cs @@ -0,0 +1,53 @@ +namespace LightlessSyncShared.Metrics; + +public class MetricsAPI +{ + public const string CounterInitializedConnections = "lightless_initialized_connections"; + public const string GaugeConnections = "lightless_connections"; + public const string GaugeAuthorizedConnections = "lightless_authorized_connections"; + public const string GaugeAvailableWorkerThreads = "lightless_available_threadpool"; + public const string GaugeAvailableIOWorkerThreads = "lightless_available_threadpool_io"; + public const string GaugeUsersRegistered = "lightless_users_registered"; + public const string CounterUsersRegisteredDeleted = "lightless_users_registered_deleted"; + public const string GaugePairs = "lightless_pairs"; + public const string GaugePairsPaused = "lightless_pairs_paused"; + public const string GaugeFilesTotal = "lightless_files"; + public const string GaugeFilesTotalColdStorage = "lightless_files_cold"; + public const string GaugeFilesTotalSize = "lightless_files_size"; + public const string GaugeFilesTotalSizeColdStorage = "lightless_files_size_cold"; + public const string GaugeFilesDownloadingFromCache = "lightless_files_downloading_from_cache"; + public const string GaugeFilesTasksWaitingForDownloadFromCache = "lightless_files_waiting_for_dl"; + public const string CounterUserPushData = "lightless_user_push"; + public const string CounterUserPushDataTo = "lightless_user_push_to"; + public const string CounterAuthenticationRequests = "lightless_auth_requests"; + public const string CounterAuthenticationCacheHits = "lightless_auth_requests_cachehit"; + public const string CounterAuthenticationFailures = "lightless_auth_requests_fail"; + public const string CounterAuthenticationSuccesses = "lightless_auth_requests_success"; + public const string GaugeAuthenticationCacheEntries = "lightless_auth_cache"; + public const string GaugeGroups = "lightless_groups"; + public const string GaugeGroupPairs = "lightless_groups_pairs"; + public const string GaugeFilesUniquePastHour = "lightless_files_unique_past_hour"; + public const string GaugeFilesUniquePastHourSize = "lightless_files_unique_past_hour_size"; + public const string GaugeFilesUniquePastDay = "lightless_files_unique_past_day"; + public const string GaugeFilesUniquePastDaySize = "lightless_files_unique_past_day_size"; + public const string GaugeCurrentDownloads = "lightless_current_downloads"; + public const string GaugeQueueFree = "lightless_download_queue_free"; + public const string GaugeQueueActive = "lightless_download_queue_active"; + public const string GaugeQueueInactive = "lightless_download_queue_inactive"; + public const string GaugeDownloadQueue = "lightless_download_queue"; + public const string GaugeDownloadQueueCancelled = "lightless_download_queue_cancelled"; + public const string GaugeDownloadPriorityQueue = "lightless_download_priority_queue"; + public const string GaugeDownloadPriorityQueueCancelled = "lightless_download_priority_queue_cancelled"; + public const string CounterFileRequests = "lightless_files_requests"; + public const string CounterFileRequestSize = "lightless_files_request_size"; + public const string CounterUserPairCacheHit = "lightless_pairscache_hit"; + public const string CounterUserPairCacheMiss = "lightless_pairscache_miss"; + public const string GaugeUserPairCacheUsers = "lightless_pairscache_users"; + public const string GaugeUserPairCacheEntries = "lightless_pairscache_entries"; + public const string CounterUserPairCacheNewEntries = "lightless_pairscache_new_entries"; + public const string CounterUserPairCacheUpdatedEntries = "lightless_pairscache_updated_entries"; + public const string GaugeGposeLobbies = "lightless_gpose_lobbies"; + public const string GaugeGposeLobbyUsers = "lightless_gpose_lobby_users"; + public const string GaugeHubConcurrency = "lightless_free_concurrent_hub_calls"; + public const string GaugeHubQueuedConcurrency = "lightless_free_concurrent_queued_hub_calls"; +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20220731210149_InitialCreate.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20220731210149_InitialCreate.Designer.cs new file mode 100644 index 0000000..6cc2ab7 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20220731210149_InitialCreate.Designer.cs @@ -0,0 +1,241 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.Models.Auth", b => + { + b.HasOne("LightlessSyncServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncServer.Models.ClientPair", b => + { + b.HasOne("LightlessSyncServer.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_uid"); + + b.HasOne("LightlessSyncServer.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("LightlessSyncServer.Models.FileCache", b => + { + b.HasOne("LightlessSyncServer.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20220731210149_InitialCreate.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20220731210149_InitialCreate.cs new file mode 100644 index 0000000..b7e4e9f --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20220731210149_InitialCreate.cs @@ -0,0 +1,163 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20220731211419_RenameLowerSnakeCase.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20220731211419_RenameLowerSnakeCase.Designer.cs new file mode 100644 index 0000000..774e5b1 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20220731211419_RenameLowerSnakeCase.Designer.cs @@ -0,0 +1,241 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.Models.Auth", b => + { + b.HasOne("LightlessSyncServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncServer.Models.ClientPair", b => + { + b.HasOne("LightlessSyncServer.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("LightlessSyncServer.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("LightlessSyncServer.Models.FileCache", b => + { + b.HasOne("LightlessSyncServer.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20220731211419_RenameLowerSnakeCase.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20220731211419_RenameLowerSnakeCase.cs new file mode 100644 index 0000000..bc2484f --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20220731211419_RenameLowerSnakeCase.cs @@ -0,0 +1,133 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20220801121419_AddLodestoneAuth.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20220801121419_AddLodestoneAuth.Designer.cs new file mode 100644 index 0000000..b0d3e13 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20220801121419_AddLodestoneAuth.Designer.cs @@ -0,0 +1,283 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.Models.Auth", b => + { + b.HasOne("LightlessSyncServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncServer.Models.ClientPair", b => + { + b.HasOne("LightlessSyncServer.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("LightlessSyncServer.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("LightlessSyncServer.Models.FileCache", b => + { + b.HasOne("LightlessSyncServer.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncServer.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20220801121419_AddLodestoneAuth.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20220801121419_AddLodestoneAuth.cs new file mode 100644 index 0000000..62ab924 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20220801121419_AddLodestoneAuth.cs @@ -0,0 +1,43 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.Designer.cs new file mode 100644 index 0000000..1f939bb --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.Designer.cs @@ -0,0 +1,283 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.Models.Auth", b => + { + b.HasOne("LightlessSyncServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncServer.Models.ClientPair", b => + { + b.HasOne("LightlessSyncServer.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("LightlessSyncServer.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("LightlessSyncServer.Models.FileCache", b => + { + b.HasOne("LightlessSyncServer.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncServer.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.cs new file mode 100644 index 0000000..c2eb10c --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20220806103053_AddBannedRegistrations.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20220806103053_AddBannedRegistrations.Designer.cs new file mode 100644 index 0000000..b958bf0 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20220806103053_AddBannedRegistrations.Designer.cs @@ -0,0 +1,295 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.Models.Auth", b => + { + b.HasOne("LightlessSyncServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncServer.Models.ClientPair", b => + { + b.HasOne("LightlessSyncServer.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("LightlessSyncServer.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("LightlessSyncServer.Models.FileCache", b => + { + b.HasOne("LightlessSyncServer.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncServer.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20220806103053_AddBannedRegistrations.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20220806103053_AddBannedRegistrations.cs new file mode 100644 index 0000000..a406826 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20220806103053_AddBannedRegistrations.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20220816170426_SetMaxLimitForStrings.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20220816170426_SetMaxLimitForStrings.Designer.cs new file mode 100644 index 0000000..dcb890f --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20220816170426_SetMaxLimitForStrings.Designer.cs @@ -0,0 +1,302 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.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("LightlessSyncServer.Models.Auth", b => + { + b.HasOne("LightlessSyncServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncServer.Models.ClientPair", b => + { + b.HasOne("LightlessSyncServer.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("LightlessSyncServer.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("LightlessSyncServer.Models.FileCache", b => + { + b.HasOne("LightlessSyncServer.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncServer.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncServer.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20220816170426_SetMaxLimitForStrings.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20220816170426_SetMaxLimitForStrings.cs new file mode 100644 index 0000000..7abb9b0 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20220816170426_SetMaxLimitForStrings.cs @@ -0,0 +1,131 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20220824225157_AddAlias.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20220824225157_AddAlias.Designer.cs new file mode 100644 index 0000000..7b56c76 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20220824225157_AddAlias.Designer.cs @@ -0,0 +1,307 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20220824225157_AddAlias.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20220824225157_AddAlias.cs new file mode 100644 index 0000000..f49bd2e --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20220824225157_AddAlias.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20220917115233_Groups.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20220917115233_Groups.Designer.cs new file mode 100644 index 0000000..90ee43b --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20220917115233_Groups.Designer.cs @@ -0,0 +1,389 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id5"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20220917115233_Groups.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20220917115233_Groups.cs new file mode 100644 index 0000000..dc3d0aa --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20220917115233_Groups.cs @@ -0,0 +1,123 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20220929150304_ChangeGidLength.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20220929150304_ChangeGidLength.Designer.cs new file mode 100644 index 0000000..b54da49 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20220929150304_ChangeGidLength.Designer.cs @@ -0,0 +1,389 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id5"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20220929150304_ChangeGidLength.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20220929150304_ChangeGidLength.cs new file mode 100644 index 0000000..376ec35 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20220929150304_ChangeGidLength.cs @@ -0,0 +1,51 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20221002105428_IsPinned.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20221002105428_IsPinned.Designer.cs new file mode 100644 index 0000000..ebacc61 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20221002105428_IsPinned.Designer.cs @@ -0,0 +1,393 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id5"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20221002105428_IsPinned.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20221002105428_IsPinned.cs new file mode 100644 index 0000000..3692716 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20221002105428_IsPinned.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20221004125939_AdjustAliasLength.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20221004125939_AdjustAliasLength.Designer.cs new file mode 100644 index 0000000..b19b402 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20221004125939_AdjustAliasLength.Designer.cs @@ -0,0 +1,393 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id5"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20221004125939_AdjustAliasLength.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20221004125939_AdjustAliasLength.cs new file mode 100644 index 0000000..632b178 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20221004125939_AdjustAliasLength.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20221006115929_GroupModerator.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20221006115929_GroupModerator.Designer.cs new file mode 100644 index 0000000..8fb8b94 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20221006115929_GroupModerator.Designer.cs @@ -0,0 +1,397 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id5"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20221006115929_GroupModerator.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20221006115929_GroupModerator.cs new file mode 100644 index 0000000..dc8a71a --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20221006115929_GroupModerator.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20221006122618_groupbans.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20221006122618_groupbans.Designer.cs new file mode 100644 index 0000000..66eac0b --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20221006122618_groupbans.Designer.cs @@ -0,0 +1,462 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id7"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id4"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id5"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20221006122618_groupbans.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20221006122618_groupbans.cs new file mode 100644 index 0000000..b1a80d1 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20221006122618_groupbans.cs @@ -0,0 +1,135 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20221024141548_GroupTempInvite.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20221024141548_GroupTempInvite.Designer.cs new file mode 100644 index 0000000..f51fe82 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20221024141548_GroupTempInvite.Designer.cs @@ -0,0 +1,501 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id7"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id4"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id5"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20221024141548_GroupTempInvite.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20221024141548_GroupTempInvite.cs new file mode 100644 index 0000000..84c41e4 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20221024141548_GroupTempInvite.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20221024181912_AdjustInviteLength.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20221024181912_AdjustInviteLength.Designer.cs new file mode 100644 index 0000000..0899683 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20221024181912_AdjustInviteLength.Designer.cs @@ -0,0 +1,501 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id7"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id4"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id5"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20221024181912_AdjustInviteLength.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20221024181912_AdjustInviteLength.cs new file mode 100644 index 0000000..dbb85f2 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20221024181912_AdjustInviteLength.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20221228033214_FileCacheSize.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20221228033214_FileCacheSize.Designer.cs new file mode 100644 index 0000000..f3d7581 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20221228033214_FileCacheSize.Designer.cs @@ -0,0 +1,506 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id7"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id4"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id5"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20221228033214_FileCacheSize.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20221228033214_FileCacheSize.cs new file mode 100644 index 0000000..2d2453a --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20221228033214_FileCacheSize.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20230111092127_IsBannedForAuth.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20230111092127_IsBannedForAuth.Designer.cs new file mode 100644 index 0000000..06feb0b --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20230111092127_IsBannedForAuth.Designer.cs @@ -0,0 +1,510 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id7"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id4"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id5"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20230111092127_IsBannedForAuth.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20230111092127_IsBannedForAuth.cs new file mode 100644 index 0000000..64a4186 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20230111092127_IsBannedForAuth.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20230118184347_FilesUploadDate.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20230118184347_FilesUploadDate.Designer.cs new file mode 100644 index 0000000..bf5d49d --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20230118184347_FilesUploadDate.Designer.cs @@ -0,0 +1,514 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id7"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id4"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id5"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20230118184347_FilesUploadDate.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20230118184347_FilesUploadDate.cs new file mode 100644 index 0000000..2e59571 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20230118184347_FilesUploadDate.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20230126163758_GroupPerms.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20230126163758_GroupPerms.Designer.cs new file mode 100644 index 0000000..a27268d --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20230126163758_GroupPerms.Designer.cs @@ -0,0 +1,530 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id7"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id4"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id5"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20230126163758_GroupPerms.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20230126163758_GroupPerms.cs new file mode 100644 index 0000000..21cbeed --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20230126163758_GroupPerms.cs @@ -0,0 +1,62 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20230131193425_AddPrimaryUserToAuth.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20230131193425_AddPrimaryUserToAuth.Designer.cs new file mode 100644 index 0000000..e66fa61 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20230131193425_AddPrimaryUserToAuth.Designer.cs @@ -0,0 +1,544 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_temp_id"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id1"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id2"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id8"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id5"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id6"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20230131193425_AddPrimaryUserToAuth.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20230131193425_AddPrimaryUserToAuth.cs new file mode 100644 index 0000000..e9810a2 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20230131193425_AddPrimaryUserToAuth.cs @@ -0,0 +1,210 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20230228001033_UserPerms.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20230228001033_UserPerms.Designer.cs new file mode 100644 index 0000000..e4ee416 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20230228001033_UserPerms.Designer.cs @@ -0,0 +1,552 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_temp_id"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id1"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id2"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id8"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id5"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id6"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20230228001033_UserPerms.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20230228001033_UserPerms.cs new file mode 100644 index 0000000..f6d92cb --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20230228001033_UserPerms.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20230319015307_UserProfileData.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20230319015307_UserProfileData.Designer.cs new file mode 100644 index 0000000..24f575c --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20230319015307_UserProfileData.Designer.cs @@ -0,0 +1,584 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_temp_id"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id1"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id2"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id8"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id5"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id6"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserProfileData", b => + { + b.HasOne("LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/Migrations/20230319015307_UserProfileData.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20230319015307_UserProfileData.cs new file mode 100644 index 0000000..18423b7 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20230319015307_UserProfileData.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20230319114005_UserProfileReports.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20230319114005_UserProfileReports.Designer.cs new file mode 100644 index 0000000..5be2d2e --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20230319114005_UserProfileReports.Designer.cs @@ -0,0 +1,650 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_temp_id"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id1"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id2"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id8"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id5"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id6"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserProfileData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserProfileDataReport", b => + { + b.HasOne("LightlessSyncShared.Models.User", "ReportedUser") + .WithMany() + .HasForeignKey("ReportedUserUID") + .HasConstraintName("fk_user_profile_data_reports_users_reported_user_uid"); + + b.HasOne("LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/Migrations/20230319114005_UserProfileReports.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20230319114005_UserProfileReports.cs new file mode 100644 index 0000000..4876a85 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20230319114005_UserProfileReports.cs @@ -0,0 +1,92 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20230420075153_DisableVFX.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20230420075153_DisableVFX.Designer.cs new file mode 100644 index 0000000..e5257df --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20230420075153_DisableVFX.Designer.cs @@ -0,0 +1,662 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_temp_id"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id1"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id2"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id8"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id5"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id6"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserProfileData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserProfileDataReport", b => + { + b.HasOne("LightlessSyncShared.Models.User", "ReportedUser") + .WithMany() + .HasForeignKey("ReportedUserUID") + .HasConstraintName("fk_user_profile_data_reports_users_reported_user_uid"); + + b.HasOne("LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/Migrations/20230420075153_DisableVFX.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20230420075153_DisableVFX.cs new file mode 100644 index 0000000..5010e0c --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20230420075153_DisableVFX.cs @@ -0,0 +1,51 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/20230924190113_permissions.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20230924190113_permissions.Designer.cs new file mode 100644 index 0000000..ad7c30d --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20230924190113_permissions.Designer.cs @@ -0,0 +1,805 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [Migration("20230924190113_permissions")] + partial class permissions + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.UserDefaultPreferredPermission", b => + { + b.Property("UserUID") + .HasColumnType("text") + .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.Property("UserUID1") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid1"); + + b.HasKey("UserUID") + .HasName("pk_user_default_preferred_permissions"); + + b.HasIndex("UserUID1") + .HasDatabaseName("ix_user_default_preferred_permissions_user_uid1"); + + b.ToTable("user_default_preferred_permissions", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.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.ToTable("user_permission_sets", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_temp_id"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id1"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id2"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id9"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id5"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id6"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id2"); + + b.HasOne("LightlessSyncShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id8"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPairPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_groups_group_temp_id1"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_users_user_temp_id7"); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserDefaultPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID1") + .HasConstraintName("fk_user_default_preferred_permissions_users_user_temp_id13"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserPermissionSet", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_permission_sets_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.UserProfileData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserProfileDataReport", b => + { + b.HasOne("LightlessSyncShared.Models.User", "ReportedUser") + .WithMany() + .HasForeignKey("ReportedUserUID") + .HasConstraintName("fk_user_profile_data_reports_users_reported_user_uid"); + + b.HasOne("LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/Migrations/20230924190113_permissions.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20230924190113_permissions.cs new file mode 100644 index 0000000..5ac10e0 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20230924190113_permissions.cs @@ -0,0 +1,442 @@ +using LightlessSyncShared.Models; +using Microsoft.EntityFrameworkCore.Migrations; +using static System.Runtime.InteropServices.JavaScript.JSType; + +#nullable disable + +namespace LightlessSyncServer.Migrations +{ + /// + public partial class permissions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "group_pair_preferred_permissions", + columns: table => new + { + group_gid = table.Column(type: "character varying(20)", nullable: false), + user_uid = table.Column(type: "character varying(10)", nullable: false), + is_paused = table.Column(type: "boolean", nullable: false), + disable_animations = table.Column(type: "boolean", nullable: false), + disable_sounds = table.Column(type: "boolean", nullable: false), + disable_vfx = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_group_pair_preferred_permissions", x => new { x.user_uid, x.group_gid }); + table.ForeignKey( + name: "fk_group_pair_preferred_permissions_groups_group_temp_id1", + column: x => x.group_gid, + principalTable: "groups", + principalColumn: "gid", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_group_pair_preferred_permissions_users_user_temp_id7", + column: x => x.user_uid, + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "user_default_preferred_permissions", + columns: table => new + { + user_uid = table.Column(type: "text", nullable: false), + user_uid1 = table.Column(type: "character varying(10)", nullable: true), + disable_individual_animations = table.Column(type: "boolean", nullable: false), + disable_individual_sounds = table.Column(type: "boolean", nullable: false), + disable_individual_vfx = table.Column(type: "boolean", nullable: false), + disable_group_animations = table.Column(type: "boolean", nullable: false), + disable_group_sounds = table.Column(type: "boolean", nullable: false), + disable_group_vfx = table.Column(type: "boolean", nullable: false), + individual_is_sticky = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_user_default_preferred_permissions", x => x.user_uid); + table.ForeignKey( + name: "fk_user_default_preferred_permissions_users_user_temp_id13", + column: x => x.user_uid1, + principalTable: "users", + principalColumn: "uid"); + }); + + migrationBuilder.CreateTable( + name: "user_permission_sets", + columns: table => new + { + user_uid = table.Column(type: "character varying(10)", nullable: false), + other_user_uid = table.Column(type: "character varying(10)", nullable: false), + sticky = table.Column(type: "boolean", nullable: false), + is_paused = table.Column(type: "boolean", nullable: false), + disable_animations = table.Column(type: "boolean", nullable: false), + disable_vfx = table.Column(type: "boolean", nullable: false), + disable_sounds = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_user_permission_sets", x => new { x.user_uid, x.other_user_uid }); + table.ForeignKey( + name: "fk_user_permission_sets_users_other_user_uid", + column: x => x.other_user_uid, + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_user_permission_sets_users_user_uid", + column: x => x.user_uid, + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.Sql(@"insert into user_permission_sets + select user1.user_uid as user_uid, user1.other_user_uid as other_user_uid, + true, + user1.is_paused as is_paused, + user1.disable_animations as disable_animations, + user1.disable_vfx as disable_vfx, + user1.disable_sounds as disable_sounds + from client_pairs as user1;"); + + migrationBuilder.Sql(@"insert into user_permission_sets + select gp.group_user_uid, gp2.group_user_uid, + false, + bool_and(gp.is_paused), + bool_and(g.disable_animations or gp.disable_animations), + bool_and(g.disable_vfx or gp.disable_vfx), + bool_and(g.disable_sounds or gp.disable_sounds) + from group_pairs gp + left join group_pairs gp2 on gp2.group_gid = gp.group_gid + left join groups g on g.gid = gp2.group_gid + where gp2.group_user_uid <> gp.group_user_uid + group by gp.group_user_uid, gp2.group_user_uid + on conflict do nothing;"); + + migrationBuilder.Sql(@"insert into group_pair_preferred_permissions + select group_gid + , group_user_uid + , gp.is_paused + , gp.disable_animations or g.disable_animations as disable_animations + , gp.disable_sounds or g.disable_sounds as disable_sounds + , gp.disable_vfx or g.disable_vfx as disable_vfx + from group_pairs as gp + left join groups g on g.gid = gp.group_gid"); + + 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.DropColumn( + name: "disable_animations", + table: "group_pairs"); + + migrationBuilder.DropColumn( + name: "disable_sounds", + table: "group_pairs"); + + migrationBuilder.DropColumn( + name: "disable_vfx", + table: "group_pairs"); + + migrationBuilder.DropColumn( + name: "is_paused", + table: "group_pairs"); + + migrationBuilder.DropColumn( + name: "allow_receiving_messages", + table: "client_pairs"); + + migrationBuilder.DropColumn( + name: "disable_animations", + table: "client_pairs"); + + migrationBuilder.DropColumn( + name: "disable_sounds", + table: "client_pairs"); + + migrationBuilder.DropColumn( + name: "disable_vfx", + table: "client_pairs"); + + migrationBuilder.DropColumn( + name: "is_paused", + table: "client_pairs"); + + migrationBuilder.RenameColumn( + name: "disable_vfx", + table: "groups", + newName: "prefer_disable_vfx"); + + migrationBuilder.RenameColumn( + name: "disable_sounds", + table: "groups", + newName: "prefer_disable_sounds"); + + migrationBuilder.RenameColumn( + name: "disable_animations", + table: "groups", + newName: "prefer_disable_animations"); + + migrationBuilder.CreateIndex( + name: "ix_group_pair_preferred_permissions_group_gid", + table: "group_pair_preferred_permissions", + column: "group_gid"); + + migrationBuilder.CreateIndex( + name: "ix_group_pair_preferred_permissions_user_uid", + table: "group_pair_preferred_permissions", + column: "user_uid"); + + migrationBuilder.CreateIndex( + name: "ix_user_default_preferred_permissions_user_uid1", + table: "user_default_preferred_permissions", + column: "user_uid1"); + + migrationBuilder.CreateIndex( + name: "ix_user_permission_sets_other_user_uid", + table: "user_permission_sets", + column: "other_user_uid"); + + migrationBuilder.CreateIndex( + name: "ix_user_permission_sets_user_uid", + table: "user_permission_sets", + column: "user_uid"); + + migrationBuilder.AddForeignKey( + name: "fk_group_pairs_groups_group_temp_id2", + table: "group_pairs", + column: "group_gid", + principalTable: "groups", + principalColumn: "gid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_group_pairs_users_group_user_temp_id8", + table: "group_pairs", + column: "group_user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_groups_users_owner_temp_id9", + table: "groups", + column: "owner_uid", + principalTable: "users", + principalColumn: "uid"); + + migrationBuilder.Sql(@"create function get_all_pairs_for_user(req_uid text) +returns table( + user_uid varchar(10) + ,other_user_uid varchar(10) + ,alias varchar(15) + ,gid varchar(20) + ,synced bool + ,ownperm_is_paused bool + ,ownperm_sticky bool + ,ownperm_disable_animations bool + ,ownperm_disable_sounds bool + ,ownperm_disable_vfx bool + ,otherperm_is_paused bool + ,otherperm_disable_animations bool + ,otherperm_disable_sounds bool + ,otherperm_disable_vfx bool) +as +$$ +begin +return query( +WITH query1 AS ( + SELECT user1.user_uid AS user_uid + ,user1.other_user_uid AS other_user_uid + ,NULL AS gid + ,NOT (user2 IS NULL) AS synced + FROM client_pairs AS user1 + LEFT JOIN client_pairs user2 ON user1.user_uid = user2.other_user_uid + AND user2.user_uid = user1.other_user_uid + WHERE user1.user_uid = req_uid +), +query2 AS ( + SELECT gp.group_user_uid + ,gp2.group_user_uid + ,gp.group_gid + ,true + FROM group_pairs gp + LEFT JOIN group_pairs gp2 ON gp2.group_gid = gp.group_gid + WHERE gp.group_user_uid = req_uid + AND gp2.group_user_uid <> req_uid + AND gp2.group_gid = gp.group_gid +) + +SELECT pairs.user_uid + ,pairs.other_user_uid + ,u.alias + ,cast(pairs.gid as varchar(20)) + ,pairs.synced + ,ownperm.is_paused + ,ownperm.sticky + ,ownperm.disable_animations + ,ownperm.disable_sounds + ,ownperm.disable_vfx + ,otherperm.is_paused + ,otherperm.disable_animations + ,otherperm.disable_sounds + ,otherperm.disable_vfx +FROM (SELECT * FROM query1 + union all + SELECT * FROM query2) AS pairs +LEFT JOIN users AS u ON pairs.other_user_uid = u.uid +LEFT JOIN user_permission_sets AS ownperm ON pairs.user_uid = ownperm.user_uid + AND pairs.other_user_uid = ownperm.other_user_uid +LEFT JOIN user_permission_sets AS otherperm ON pairs.user_uid = otherperm.other_user_uid + AND pairs.other_user_uid = otherperm.user_uid +WHERE pairs.user_uid = req_uid + AND u.uid = pairs.other_user_uid + AND ( + (ownperm.user_uid = req_uid) + OR (otherperm.other_user_uid = req_uid) + ) +); +end; +$$ +language plpgsql;"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_group_pairs_groups_group_temp_id2", + table: "group_pairs"); + + migrationBuilder.DropForeignKey( + name: "fk_group_pairs_users_group_user_temp_id8", + table: "group_pairs"); + + migrationBuilder.DropForeignKey( + name: "fk_groups_users_owner_temp_id9", + table: "groups"); + + migrationBuilder.DropTable( + name: "group_pair_preferred_permissions"); + + migrationBuilder.DropTable( + name: "user_default_preferred_permissions"); + + migrationBuilder.DropTable( + name: "user_permission_sets"); + + migrationBuilder.RenameColumn( + name: "prefer_disable_vfx", + table: "groups", + newName: "disable_vfx"); + + migrationBuilder.RenameColumn( + name: "prefer_disable_sounds", + table: "groups", + newName: "disable_sounds"); + + migrationBuilder.RenameColumn( + name: "prefer_disable_animations", + table: "groups", + newName: "disable_animations"); + + 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); + + migrationBuilder.AddColumn( + name: "disable_vfx", + table: "group_pairs", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "is_paused", + table: "group_pairs", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "allow_receiving_messages", + table: "client_pairs", + type: "boolean", + nullable: false, + defaultValue: false); + + 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); + + migrationBuilder.AddColumn( + name: "disable_vfx", + table: "client_pairs", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "is_paused", + table: "client_pairs", + type: "boolean", + nullable: false, + defaultValue: false); + + 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/LightlessSyncServer/LightlessSyncShared/Migrations/20230926212023_AlterPermissions.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20230926212023_AlterPermissions.Designer.cs new file mode 100644 index 0000000..710a836 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20230926212023_AlterPermissions.Designer.cs @@ -0,0 +1,868 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [Migration("20230926212023_AlterPermissions")] + partial class AlterPermissions + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.UserPermissionQuery", b => + { + b.Property("Alias") + .HasColumnType("text") + .HasColumnName("alias"); + + b.Property("GID") + .HasColumnType("text") + .HasColumnName("gid"); + + b.Property("OtherUserUID") + .HasColumnType("text") + .HasColumnName("other_user_uid"); + + b.Property("OtherpermDisableAnimations") + .HasColumnType("boolean") + .HasColumnName("otherperm_disable_animations"); + + b.Property("OtherpermDisableSounds") + .HasColumnType("boolean") + .HasColumnName("otherperm_disable_sounds"); + + b.Property("OtherpermDisableVFX") + .HasColumnType("boolean") + .HasColumnName("otherperm_disable_vfx"); + + b.Property("OtherpermIsPaused") + .HasColumnType("boolean") + .HasColumnName("otherperm_is_paused"); + + b.Property("OwnPermSticky") + .HasColumnType("boolean") + .HasColumnName("own_perm_sticky"); + + b.Property("OwnpermDisableAnimations") + .HasColumnType("boolean") + .HasColumnName("ownperm_disable_animations"); + + b.Property("OwnpermDisableSounds") + .HasColumnType("boolean") + .HasColumnName("ownperm_disable_sounds"); + + b.Property("OwnpermDisableVFX") + .HasColumnType("boolean") + .HasColumnName("ownperm_disable_vfx"); + + b.Property("OwnpermIsPaused") + .HasColumnType("boolean") + .HasColumnName("ownperm_is_paused"); + + b.Property("Synced") + .HasColumnType("boolean") + .HasColumnName("synced"); + + b.Property("UserUID") + .HasColumnType("text") + .HasColumnName("user_uid"); + + b.ToTable("user_permission_query", null, t => + { + t.ExcludeFromMigrations(); + }); + }); + + modelBuilder.Entity("LightlessSyncShared.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.ToTable("user_permission_sets", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_temp_id"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id1"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id2"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id9"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id5"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id6"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id2"); + + b.HasOne("LightlessSyncShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id8"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPairPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_groups_group_temp_id1"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_users_user_temp_id7"); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserDefaultPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_default_preferred_permissions_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserPermissionSet", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_permission_sets_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.UserProfileData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserProfileDataReport", b => + { + b.HasOne("LightlessSyncShared.Models.User", "ReportedUser") + .WithMany() + .HasForeignKey("ReportedUserUID") + .HasConstraintName("fk_user_profile_data_reports_users_reported_user_uid"); + + b.HasOne("LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/Migrations/20230926212023_AlterPermissions.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20230926212023_AlterPermissions.cs new file mode 100644 index 0000000..3480ff2 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20230926212023_AlterPermissions.cs @@ -0,0 +1,87 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.Migrations +{ + /// + public partial class AlterPermissions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_user_default_preferred_permissions_users_user_temp_id13", + table: "user_default_preferred_permissions"); + + migrationBuilder.DropIndex( + name: "ix_user_default_preferred_permissions_user_uid1", + table: "user_default_preferred_permissions"); + + migrationBuilder.DropColumn( + name: "user_uid1", + table: "user_default_preferred_permissions"); + + migrationBuilder.AlterColumn( + name: "user_uid", + table: "user_default_preferred_permissions", + type: "character varying(10)", + maxLength: 10, + nullable: false, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.CreateIndex( + name: "ix_user_default_preferred_permissions_user_uid", + table: "user_default_preferred_permissions", + column: "user_uid"); + + migrationBuilder.AddForeignKey( + name: "fk_user_default_preferred_permissions_users_user_uid", + table: "user_default_preferred_permissions", + column: "user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_user_default_preferred_permissions_users_user_uid", + table: "user_default_preferred_permissions"); + + migrationBuilder.DropIndex( + name: "ix_user_default_preferred_permissions_user_uid", + table: "user_default_preferred_permissions"); + + migrationBuilder.AlterColumn( + name: "user_uid", + table: "user_default_preferred_permissions", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(10)", + oldMaxLength: 10); + + migrationBuilder.AddColumn( + name: "user_uid1", + table: "user_default_preferred_permissions", + type: "character varying(10)", + nullable: true); + + migrationBuilder.CreateIndex( + name: "ix_user_default_preferred_permissions_user_uid1", + table: "user_default_preferred_permissions", + column: "user_uid1"); + + migrationBuilder.AddForeignKey( + name: "fk_user_default_preferred_permissions_users_user_temp_id13", + table: "user_default_preferred_permissions", + column: "user_uid1", + principalTable: "users", + principalColumn: "uid"); + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20231027090903_PausedIndex.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20231027090903_PausedIndex.Designer.cs new file mode 100644 index 0000000..1cef86c --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20231027090903_PausedIndex.Designer.cs @@ -0,0 +1,807 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [Migration("20231027090903_PausedIndex")] + partial class PausedIndex + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_temp_id"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id1"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id2"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id9"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id5"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id6"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id2"); + + b.HasOne("LightlessSyncShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id8"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPairPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_groups_group_temp_id1"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_users_user_temp_id7"); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserDefaultPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_default_preferred_permissions_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserPermissionSet", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_permission_sets_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.UserProfileData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserProfileDataReport", b => + { + b.HasOne("LightlessSyncShared.Models.User", "ReportedUser") + .WithMany() + .HasForeignKey("ReportedUserUID") + .HasConstraintName("fk_user_profile_data_reports_users_reported_user_uid"); + + b.HasOne("LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/Migrations/20231027090903_PausedIndex.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20231027090903_PausedIndex.cs new file mode 100644 index 0000000..3e49cbe --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20231027090903_PausedIndex.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.Migrations +{ + /// + public partial class PausedIndex : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "ix_user_permission_sets_user_uid_other_user_uid_is_paused", + table: "user_permission_sets", + columns: new[] { "user_uid", "other_user_uid", "is_paused" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_user_permission_sets_user_uid_other_user_uid_is_paused", + table: "user_permission_sets"); + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20240718095806_MarkForBan.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20240718095806_MarkForBan.Designer.cs new file mode 100644 index 0000000..457a2f1 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20240718095806_MarkForBan.Designer.cs @@ -0,0 +1,811 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [Migration("20240718095806_MarkForBan")] + partial class MarkForBan + { + /// + 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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_uid"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_uid"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_gid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPairPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_groups_group_gid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserDefaultPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_default_preferred_permissions_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserPermissionSet", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_permission_sets_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.UserProfileData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserProfileDataReport", b => + { + b.HasOne("LightlessSyncShared.Models.User", "ReportedUser") + .WithMany() + .HasForeignKey("ReportedUserUID") + .HasConstraintName("fk_user_profile_data_reports_users_reported_user_uid"); + + b.HasOne("LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/Migrations/20240718095806_MarkForBan.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20240718095806_MarkForBan.cs new file mode 100644 index 0000000..e5c866d --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20240718095806_MarkForBan.cs @@ -0,0 +1,309 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.Migrations +{ + /// + public partial class MarkForBan : 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_pair_preferred_permissions_groups_group_temp_id1", + table: "group_pair_preferred_permissions"); + + migrationBuilder.DropForeignKey( + name: "fk_group_pair_preferred_permissions_users_user_temp_id7", + table: "group_pair_preferred_permissions"); + + migrationBuilder.DropForeignKey( + name: "fk_group_pairs_groups_group_temp_id2", + table: "group_pairs"); + + migrationBuilder.DropForeignKey( + name: "fk_group_pairs_users_group_user_temp_id8", + table: "group_pairs"); + + migrationBuilder.DropForeignKey( + name: "fk_groups_users_owner_temp_id9", + table: "groups"); + + migrationBuilder.AddColumn( + name: "mark_for_ban", + table: "auth", + type: "boolean", + nullable: false, + defaultValue: false); + + 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_pair_preferred_permissions_groups_group_gid", + table: "group_pair_preferred_permissions", + column: "group_gid", + principalTable: "groups", + principalColumn: "gid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_group_pair_preferred_permissions_users_user_uid", + table: "group_pair_preferred_permissions", + column: "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_pair_preferred_permissions_groups_group_gid", + table: "group_pair_preferred_permissions"); + + migrationBuilder.DropForeignKey( + name: "fk_group_pair_preferred_permissions_users_user_uid", + table: "group_pair_preferred_permissions"); + + 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.DropColumn( + name: "mark_for_ban", + table: "auth"); + + 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_pair_preferred_permissions_groups_group_temp_id1", + table: "group_pair_preferred_permissions", + column: "group_gid", + principalTable: "groups", + principalColumn: "gid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_group_pair_preferred_permissions_users_user_temp_id7", + table: "group_pair_preferred_permissions", + column: "user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_group_pairs_groups_group_temp_id2", + table: "group_pairs", + column: "group_gid", + principalTable: "groups", + principalColumn: "gid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_group_pairs_users_group_user_temp_id8", + table: "group_pairs", + column: "group_user_uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_groups_users_owner_temp_id9", + table: "groups", + column: "owner_uid", + principalTable: "users", + principalColumn: "uid"); + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20240718100453_RemoveReport.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20240718100453_RemoveReport.Designer.cs new file mode 100644 index 0000000..35f6307 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20240718100453_RemoveReport.Designer.cs @@ -0,0 +1,757 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [Migration("20240718100453_RemoveReport")] + partial class RemoveReport + { + /// + 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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_uid"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_uid"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_gid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPairPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_groups_group_gid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserDefaultPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_default_preferred_permissions_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserPermissionSet", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_permission_sets_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.UserProfileData", b => + { + b.HasOne("LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/Migrations/20240718100453_RemoveReport.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20240718100453_RemoveReport.cs new file mode 100644 index 0000000..b4aff70 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20240718100453_RemoveReport.cs @@ -0,0 +1,59 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LightlessSyncServer.Migrations +{ + /// + public partial class RemoveReport : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "user_profile_data_reports"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "user_profile_data_reports", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + reported_user_uid = table.Column(type: "character varying(10)", nullable: true), + reporting_user_uid = table.Column(type: "character varying(10)", nullable: true), + report_date = table.Column(type: "timestamp with time zone", nullable: false), + 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"); + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20240904144711_AddRawFileSize.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20240904144711_AddRawFileSize.Designer.cs new file mode 100644 index 0000000..95f39a4 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20240904144711_AddRawFileSize.Designer.cs @@ -0,0 +1,761 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [Migration("20240904144711_AddRawFileSize")] + partial class AddRawFileSize + { + /// + 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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_uid"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_uid"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_gid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPairPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_groups_group_gid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserDefaultPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_default_preferred_permissions_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserPermissionSet", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_permission_sets_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.UserProfileData", b => + { + b.HasOne("LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/Migrations/20240904144711_AddRawFileSize.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20240904144711_AddRawFileSize.cs new file mode 100644 index 0000000..27d3548 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20240904144711_AddRawFileSize.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.Migrations +{ + /// + public partial class AddRawFileSize : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "raw_size", + table: "file_caches", + type: "bigint", + nullable: false, + defaultValue: 0L); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "raw_size", + table: "file_caches"); + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20241226112428_CharaData.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20241226112428_CharaData.Designer.cs new file mode 100644 index 0000000..9dabfe9 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20241226112428_CharaData.Designer.cs @@ -0,0 +1,1024 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [Migration("20241226112428_CharaData")] + partial class CharaData + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("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("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataFile", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("GamePath") + .HasColumnType("text") + .HasColumnName("game_path"); + + b.Property("FileCacheHash") + .HasColumnType("character varying(40)") + .HasColumnName("file_cache_hash"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.HasKey("ParentId", "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.HasIndex("ParentId", "ParentUploaderUID") + .HasDatabaseName("ix_chara_data_files_parent_id_parent_uploader_uid"); + + b.ToTable("chara_data_files", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataOriginalFile", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("Hash") + .HasColumnType("text") + .HasColumnName("hash"); + + b.HasKey("ParentId", "ParentUploaderUID", "Hash") + .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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_uid"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataAllowance", b => + { + b.HasOne("LightlessSyncShared.Models.User", "AllowedUser") + .WithMany() + .HasForeignKey("AllowedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_allowance_users_allowed_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataFile", b => + { + b.HasOne("LightlessSyncShared.Models.FileCache", "FileCache") + .WithMany() + .HasForeignKey("FileCacheHash") + .HasConstraintName("fk_chara_data_files_files_file_cache_hash"); + + b.HasOne("LightlessSyncShared.Models.CharaData", "Parent") + .WithMany("Files") + .HasForeignKey("ParentId", "ParentUploaderUID") + .HasConstraintName("fk_chara_data_files_chara_data_parent_id_parent_uploader_uid"); + + b.Navigation("FileCache"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataOriginalFile", b => + { + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataPose", b => + { + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_uid"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_gid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPairPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_groups_group_gid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserDefaultPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_default_preferred_permissions_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserPermissionSet", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_permission_sets_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.UserProfileData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaData", b => + { + b.Navigation("AllowedIndividiuals"); + + b.Navigation("Files"); + + b.Navigation("OriginalFiles"); + + b.Navigation("Poses"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20241226112428_CharaData.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20241226112428_CharaData.cs new file mode 100644 index 0000000..8708151 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20241226112428_CharaData.cs @@ -0,0 +1,198 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LightlessSyncServer.Migrations +{ + /// + public partial class CharaData : 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), + 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_files", + columns: table => new + { + game_path = table.Column(type: "text", nullable: false), + parent_id = table.Column(type: "text", nullable: false), + file_cache_hash = table.Column(type: "character varying(40)", nullable: true), + parent_uploader_uid = table.Column(type: "character varying(10)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_chara_data_files", x => new { x.parent_id, 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" }); + table.ForeignKey( + name: "fk_chara_data_files_files_file_cache_hash", + column: x => x.file_cache_hash, + principalTable: "file_caches", + principalColumn: "hash"); + }); + + 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), + hash = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_chara_data_orig_files", x => new { x.parent_id, x.parent_uploader_uid, x.hash }); + 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_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_files_parent_id_parent_uploader_uid", + table: "chara_data_files", + columns: new[] { "parent_id", "parent_uploader_uid" }); + + 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_files"); + + migrationBuilder.DropTable( + name: "chara_data_orig_files"); + + migrationBuilder.DropTable( + name: "chara_data_poses"); + + migrationBuilder.DropTable( + name: "chara_data"); + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20241226194944_CharaDataFileSwap.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20241226194944_CharaDataFileSwap.Designer.cs new file mode 100644 index 0000000..b8045bb --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20241226194944_CharaDataFileSwap.Designer.cs @@ -0,0 +1,1064 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [Migration("20241226194944_CharaDataFileSwap")] + partial class CharaDataFileSwap + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataOriginalFile", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("Hash") + .HasColumnType("text") + .HasColumnName("hash"); + + b.HasKey("ParentId", "ParentUploaderUID", "Hash") + .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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_uid"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataAllowance", b => + { + b.HasOne("LightlessSyncShared.Models.User", "AllowedUser") + .WithMany() + .HasForeignKey("AllowedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_allowance_users_allowed_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataFile", b => + { + b.HasOne("LightlessSyncShared.Models.FileCache", "FileCache") + .WithMany() + .HasForeignKey("FileCacheHash") + .HasConstraintName("fk_chara_data_files_files_file_cache_hash"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataFileSwap", b => + { + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataOriginalFile", b => + { + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataPose", b => + { + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_uid"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_gid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPairPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_groups_group_gid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserDefaultPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_default_preferred_permissions_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserPermissionSet", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_permission_sets_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.UserProfileData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/Migrations/20241226194944_CharaDataFileSwap.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20241226194944_CharaDataFileSwap.cs new file mode 100644 index 0000000..6414e12 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20241226194944_CharaDataFileSwap.cs @@ -0,0 +1,114 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.Migrations +{ + /// + public partial class CharaDataFileSwap : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_chara_data_files_chara_data_parent_id_parent_uploader_uid", + table: "chara_data_files"); + + migrationBuilder.DropPrimaryKey( + name: "pk_chara_data_files", + table: "chara_data_files"); + + migrationBuilder.DropIndex( + name: "ix_chara_data_files_parent_id_parent_uploader_uid", + table: "chara_data_files"); + + migrationBuilder.AlterColumn( + name: "parent_uploader_uid", + table: "chara_data_files", + type: "character varying(10)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "character varying(10)", + oldNullable: true); + + migrationBuilder.AddPrimaryKey( + name: "pk_chara_data_files", + table: "chara_data_files", + columns: new[] { "parent_id", "parent_uploader_uid", "game_path" }); + + 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.CreateIndex( + name: "ix_chara_data_file_swaps_parent_id", + table: "chara_data_file_swaps", + column: "parent_id"); + + migrationBuilder.AddForeignKey( + name: "fk_chara_data_files_chara_data_parent_id_parent_uploader_uid", + table: "chara_data_files", + columns: new[] { "parent_id", "parent_uploader_uid" }, + principalTable: "chara_data", + principalColumns: new[] { "id", "uploader_uid" }, + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_chara_data_files_chara_data_parent_id_parent_uploader_uid", + table: "chara_data_files"); + + migrationBuilder.DropTable( + name: "chara_data_file_swaps"); + + migrationBuilder.DropPrimaryKey( + name: "pk_chara_data_files", + table: "chara_data_files"); + + migrationBuilder.AlterColumn( + name: "parent_uploader_uid", + table: "chara_data_files", + type: "character varying(10)", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(10)"); + + migrationBuilder.AddPrimaryKey( + name: "pk_chara_data_files", + table: "chara_data_files", + columns: new[] { "parent_id", "game_path" }); + + migrationBuilder.CreateIndex( + name: "ix_chara_data_files_parent_id_parent_uploader_uid", + table: "chara_data_files", + columns: new[] { "parent_id", "parent_uploader_uid" }); + + migrationBuilder.AddForeignKey( + name: "fk_chara_data_files_chara_data_parent_id_parent_uploader_uid", + table: "chara_data_files", + columns: new[] { "parent_id", "parent_uploader_uid" }, + principalTable: "chara_data", + principalColumns: new[] { "id", "uploader_uid" }); + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20241227130901_CascadeFile.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20241227130901_CascadeFile.Designer.cs new file mode 100644 index 0000000..de1e266 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20241227130901_CascadeFile.Designer.cs @@ -0,0 +1,1065 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [Migration("20241227130901_CascadeFile")] + partial class CascadeFile + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataOriginalFile", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("Hash") + .HasColumnType("text") + .HasColumnName("hash"); + + b.HasKey("ParentId", "ParentUploaderUID", "Hash") + .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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_uid"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataAllowance", b => + { + b.HasOne("LightlessSyncShared.Models.User", "AllowedUser") + .WithMany() + .HasForeignKey("AllowedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_allowance_users_allowed_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataFile", b => + { + b.HasOne("LightlessSyncShared.Models.FileCache", "FileCache") + .WithMany() + .HasForeignKey("FileCacheHash") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_files_files_file_cache_hash"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataFileSwap", b => + { + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataOriginalFile", b => + { + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataPose", b => + { + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_uid"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_gid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPairPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_groups_group_gid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserDefaultPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_default_preferred_permissions_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserPermissionSet", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_permission_sets_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.UserProfileData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/Migrations/20241227130901_CascadeFile.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20241227130901_CascadeFile.cs new file mode 100644 index 0000000..ad53d9b --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20241227130901_CascadeFile.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.Migrations +{ + /// + public partial class CascadeFile : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_chara_data_files_files_file_cache_hash", + table: "chara_data_files"); + + migrationBuilder.AddForeignKey( + name: "fk_chara_data_files_files_file_cache_hash", + table: "chara_data_files", + column: "file_cache_hash", + principalTable: "file_caches", + principalColumn: "hash", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_chara_data_files_files_file_cache_hash", + table: "chara_data_files"); + + migrationBuilder.AddForeignKey( + name: "fk_chara_data_files_files_file_cache_hash", + table: "chara_data_files", + column: "file_cache_hash", + principalTable: "file_caches", + principalColumn: "hash"); + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20241227190944_OrigFileGamePath.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20241227190944_OrigFileGamePath.Designer.cs new file mode 100644 index 0000000..09f5a66 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20241227190944_OrigFileGamePath.Designer.cs @@ -0,0 +1,1069 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [Migration("20241227190944_OrigFileGamePath")] + partial class OrigFileGamePath + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_uid"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataAllowance", b => + { + b.HasOne("LightlessSyncShared.Models.User", "AllowedUser") + .WithMany() + .HasForeignKey("AllowedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_allowance_users_allowed_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataFile", b => + { + b.HasOne("LightlessSyncShared.Models.FileCache", "FileCache") + .WithMany() + .HasForeignKey("FileCacheHash") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_files_files_file_cache_hash"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataFileSwap", b => + { + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataOriginalFile", b => + { + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataPose", b => + { + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_uid"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_gid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPairPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_groups_group_gid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserDefaultPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_default_preferred_permissions_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserPermissionSet", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_permission_sets_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.UserProfileData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/Migrations/20241227190944_OrigFileGamePath.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20241227190944_OrigFileGamePath.cs new file mode 100644 index 0000000..2f571b7 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20241227190944_OrigFileGamePath.cs @@ -0,0 +1,65 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.Migrations +{ + /// + public partial class OrigFileGamePath : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropPrimaryKey( + name: "pk_chara_data_orig_files", + table: "chara_data_orig_files"); + + migrationBuilder.AlterColumn( + name: "hash", + table: "chara_data_orig_files", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AddColumn( + name: "game_path", + table: "chara_data_orig_files", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddPrimaryKey( + name: "pk_chara_data_orig_files", + table: "chara_data_orig_files", + columns: new[] { "parent_id", "parent_uploader_uid", "game_path" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropPrimaryKey( + name: "pk_chara_data_orig_files", + table: "chara_data_orig_files"); + + migrationBuilder.DropColumn( + name: "game_path", + table: "chara_data_orig_files"); + + migrationBuilder.AlterColumn( + name: "hash", + table: "chara_data_orig_files", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AddPrimaryKey( + name: "pk_chara_data_orig_files", + table: "chara_data_orig_files", + columns: new[] { "parent_id", "parent_uploader_uid", "hash" }); + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20241228190750_ManipData.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20241228190750_ManipData.Designer.cs new file mode 100644 index 0000000..13ac255 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20241228190750_ManipData.Designer.cs @@ -0,0 +1,1073 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [Migration("20241228190750_ManipData")] + partial class ManipData + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_uid"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataAllowance", b => + { + b.HasOne("LightlessSyncShared.Models.User", "AllowedUser") + .WithMany() + .HasForeignKey("AllowedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_allowance_users_allowed_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataFile", b => + { + b.HasOne("LightlessSyncShared.Models.FileCache", "FileCache") + .WithMany() + .HasForeignKey("FileCacheHash") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_files_files_file_cache_hash"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataFileSwap", b => + { + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataOriginalFile", b => + { + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataPose", b => + { + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_uid"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_gid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPairPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_groups_group_gid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserDefaultPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_default_preferred_permissions_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserPermissionSet", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_permission_sets_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.UserProfileData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/Migrations/20241228190750_ManipData.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20241228190750_ManipData.cs new file mode 100644 index 0000000..4b38940 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20241228190750_ManipData.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.Migrations +{ + /// + public partial class ManipData : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "manipulation_data", + table: "chara_data", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "manipulation_data", + table: "chara_data"); + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20250112111727_AllowedGroup.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20250112111727_AllowedGroup.Designer.cs new file mode 100644 index 0000000..066d6e3 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20250112111727_AllowedGroup.Designer.cs @@ -0,0 +1,1094 @@ +// +using System; +using LightlessSyncShared.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 LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [Migration("20250112111727_AllowedGroup")] + partial class AllowedGroup + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_uid"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataAllowance", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "AllowedGroup") + .WithMany() + .HasForeignKey("AllowedGroupGID") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_allowance_groups_allowed_group_gid"); + + b.HasOne("LightlessSyncShared.Models.User", "AllowedUser") + .WithMany() + .HasForeignKey("AllowedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_allowance_users_allowed_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataFile", b => + { + b.HasOne("LightlessSyncShared.Models.FileCache", "FileCache") + .WithMany() + .HasForeignKey("FileCacheHash") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_files_files_file_cache_hash"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataFileSwap", b => + { + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataOriginalFile", b => + { + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataPose", b => + { + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_uid"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_gid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPairPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_groups_group_gid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserDefaultPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_default_preferred_permissions_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserPermissionSet", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_permission_sets_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.UserProfileData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/Migrations/20250112111727_AllowedGroup.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20250112111727_AllowedGroup.cs new file mode 100644 index 0000000..a908cca --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20250112111727_AllowedGroup.cs @@ -0,0 +1,98 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LightlessSyncServer.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/LightlessSyncServer/LightlessSyncShared/Migrations/MareDbContextModelSnapshot.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/MareDbContextModelSnapshot.cs new file mode 100644 index 0000000..f5f0cdc --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/MareDbContextModelSnapshot.cs @@ -0,0 +1,1091 @@ +// +using System; +using LightlessSyncShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + partial class LightlessDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.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("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_uid"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataAllowance", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "AllowedGroup") + .WithMany() + .HasForeignKey("AllowedGroupGID") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_allowance_groups_allowed_group_gid"); + + b.HasOne("LightlessSyncShared.Models.User", "AllowedUser") + .WithMany() + .HasForeignKey("AllowedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_allowance_users_allowed_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataFile", b => + { + b.HasOne("LightlessSyncShared.Models.FileCache", "FileCache") + .WithMany() + .HasForeignKey("FileCacheHash") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_files_files_file_cache_hash"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataFileSwap", b => + { + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataOriginalFile", b => + { + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.CharaDataPose", b => + { + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_uid"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_gid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupPairPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_groups_group_gid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserDefaultPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_default_preferred_permissions_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserPermissionSet", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_permission_sets_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.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("LightlessSyncShared.Models.UserProfileData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/Models/Auth.cs b/LightlessSyncServer/LightlessSyncShared/Models/Auth.cs new file mode 100644 index 0000000..80dfb81 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Models/Auth.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; + +namespace LightlessSyncShared.Models; + +public class Auth +{ + [Key] + [MaxLength(64)] + public string HashedKey { get; set; } + + public string UserUID { get; set; } + public User User { get; set; } + public bool MarkForBan { get; set; } + public bool IsBanned { get; set; } + public string? PrimaryUserUID { get; set; } + public User? PrimaryUser { get; set; } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Models/Banned.cs b/LightlessSyncServer/LightlessSyncShared/Models/Banned.cs new file mode 100644 index 0000000..25886e0 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Models/Banned.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/Models/BannedRegistrations.cs b/LightlessSyncServer/LightlessSyncShared/Models/BannedRegistrations.cs new file mode 100644 index 0000000..f031fa3 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Models/BannedRegistrations.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace LightlessSyncShared.Models; + +public class BannedRegistrations +{ + [Key] + [MaxLength(100)] + public string DiscordIdOrLodestoneAuth { get; set; } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Models/CharaData.cs b/LightlessSyncServer/LightlessSyncShared/Models/CharaData.cs new file mode 100644 index 0000000..0ad3336 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Models/CharaData.cs @@ -0,0 +1,91 @@ +using System.ComponentModel.DataAnnotations; + +namespace LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/Models/ClientPair.cs b/LightlessSyncServer/LightlessSyncShared/Models/ClientPair.cs new file mode 100644 index 0000000..198f5e2 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Models/ClientPair.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace LightlessSyncShared.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; } + [Timestamp] + public byte[] Timestamp { get; set; } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Models/FileCache.cs b/LightlessSyncServer/LightlessSyncShared/Models/FileCache.cs new file mode 100644 index 0000000..cd52691 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Models/FileCache.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace LightlessSyncShared.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; } + public long RawSize { get; set; } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Models/ForbiddenUploadEntry.cs b/LightlessSyncServer/LightlessSyncShared/Models/ForbiddenUploadEntry.cs new file mode 100644 index 0000000..2d991c2 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Models/ForbiddenUploadEntry.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/Models/Group.cs b/LightlessSyncServer/LightlessSyncShared/Models/Group.cs new file mode 100644 index 0000000..67de63d --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Models/Group.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace LightlessSyncShared.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 PreferDisableSounds { get; set; } + public bool PreferDisableAnimations { get; set; } + public bool PreferDisableVFX { get; set; } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Models/GroupBan.cs b/LightlessSyncServer/LightlessSyncShared/Models/GroupBan.cs new file mode 100644 index 0000000..1bdcaf1 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Models/GroupBan.cs @@ -0,0 +1,13 @@ +namespace LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/Models/GroupPair.cs b/LightlessSyncServer/LightlessSyncShared/Models/GroupPair.cs new file mode 100644 index 0000000..bb5824e --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Models/GroupPair.cs @@ -0,0 +1,11 @@ +namespace LightlessSyncShared.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 IsPinned { get; set; } + public bool IsModerator { get; set; } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Models/GroupPairPreferredPermission.cs b/LightlessSyncServer/LightlessSyncShared/Models/GroupPairPreferredPermission.cs new file mode 100644 index 0000000..6946810 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Models/GroupPairPreferredPermission.cs @@ -0,0 +1,13 @@ +namespace LightlessSyncShared.Models; + +public class GroupPairPreferredPermission +{ + public string GroupGID { get; set; } + public Group Group { get; set; } + public string UserUID { get; set; } + public User User { get; set; } + public bool IsPaused { get; set; } + public bool DisableAnimations { get; set; } + public bool DisableSounds { get; set; } + public bool DisableVFX { get; set; } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Models/GroupTempInvite.cs b/LightlessSyncServer/LightlessSyncShared/Models/GroupTempInvite.cs new file mode 100644 index 0000000..548258d --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Models/GroupTempInvite.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/Models/LodeStoneAuth.cs b/LightlessSyncServer/LightlessSyncShared/Models/LodeStoneAuth.cs new file mode 100644 index 0000000..616ab09 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Models/LodeStoneAuth.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/Models/User.cs b/LightlessSyncServer/LightlessSyncShared/Models/User.cs new file mode 100644 index 0000000..fcaef37 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Models/User.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/Models/UserDefaultPreferredPermission.cs b/LightlessSyncServer/LightlessSyncShared/Models/UserDefaultPreferredPermission.cs new file mode 100644 index 0000000..0c49454 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Models/UserDefaultPreferredPermission.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace LightlessSyncShared.Models; + +public class UserDefaultPreferredPermission +{ + [Key] + [MaxLength(10)] + [ForeignKey("User")] + public string UserUID { get; set; } + public User User { get; set; } + + public bool DisableIndividualAnimations { get; set; } = false; + public bool DisableIndividualSounds { get; set; } = false; + public bool DisableIndividualVFX { get; set; } = false; + public bool DisableGroupAnimations { get; set; } = false; + public bool DisableGroupSounds { get; set; } = false; + public bool DisableGroupVFX { get; set; } = false; + public bool IndividualIsSticky { get; set; } = false; +} + diff --git a/LightlessSyncServer/LightlessSyncShared/Models/UserPermissionQuery.cs b/LightlessSyncServer/LightlessSyncShared/Models/UserPermissionQuery.cs new file mode 100644 index 0000000..3873a46 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Models/UserPermissionQuery.cs @@ -0,0 +1,40 @@ +namespace LightlessSyncShared.Models; + +public class UserPermissionQuery +{ + public string UserUID { get; set; } + public string OtherUserUID { get; set; } + public string Alias { get; set; } + public string GID { get; set; } + public bool Synced { get; set; } + public bool? OwnpermIsPaused { get; set; } + public bool? OwnpermSticky { get; set; } + public bool? OwnpermDisableAnimations { get; set; } + public bool? OwnpermDisableSounds { get; set; } + public bool? OwnpermDisableVFX { get; set; } + public bool? OtherpermIsPaused { get; set; } + public bool? OtherpermDisableAnimations { get; set; } + public bool? OtherpermDisableSounds { get; set; } + public bool? OtherpermDisableVFX { get; set; } + + public UserPermissionSet? OwnPermissions => OwnpermSticky == null ? null : new UserPermissionSet + { + UserUID = UserUID, + OtherUserUID = OtherUserUID, + IsPaused = OwnpermIsPaused.Value, + DisableAnimations = OwnpermDisableAnimations.Value, + DisableSounds = OwnpermDisableSounds.Value, + DisableVFX = OwnpermDisableVFX.Value, + Sticky = OwnpermSticky.Value + }; + + public UserPermissionSet? OtherPermissions => !Synced ? null : new UserPermissionSet + { + UserUID = OtherUserUID, + OtherUserUID = UserUID, + IsPaused = OtherpermIsPaused ?? false, + DisableAnimations = OtherpermDisableAnimations ?? false, + DisableSounds = OtherpermDisableSounds ?? false, + DisableVFX = OtherpermDisableVFX ?? false, + }; +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncShared/Models/UserPermissionSet.cs b/LightlessSyncServer/LightlessSyncShared/Models/UserPermissionSet.cs new file mode 100644 index 0000000..5a00f3a --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Models/UserPermissionSet.cs @@ -0,0 +1,18 @@ +using System.Diagnostics.CodeAnalysis; + +namespace LightlessSyncShared.Models; + +public class UserPermissionSet +{ + [NotNull] + public string UserUID { get; set; } + public User User { get; set; } + [NotNull] + public string OtherUserUID { get; set; } + public User OtherUser { get; set; } + public bool Sticky { get; set; } = false; + public bool IsPaused { get; set; } = false; + public bool DisableAnimations { get; set; } = false; + public bool DisableVFX { get; set; } = false; + public bool DisableSounds { get; set; } = false; +} diff --git a/LightlessSyncServer/LightlessSyncShared/Models/UserProfileData.cs b/LightlessSyncServer/LightlessSyncShared/Models/UserProfileData.cs new file mode 100644 index 0000000..0029b0e --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Models/UserProfileData.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/ExistingUserRequirement.cs b/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/ExistingUserRequirement.cs new file mode 100644 index 0000000..e0132af --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/ExistingUserRequirement.cs @@ -0,0 +1,5 @@ +using Microsoft.AspNetCore.Authorization; + +namespace LightlessSyncShared.RequirementHandlers; + +public class ExistingUserRequirement : IAuthorizationRequirement { } \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/ExistingUserRequirementHandler.cs b/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/ExistingUserRequirementHandler.cs new file mode 100644 index 0000000..f2b1e49 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/ExistingUserRequirementHandler.cs @@ -0,0 +1,84 @@ +using LightlessSyncShared.Data; +using LightlessSyncShared.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; + +namespace LightlessSyncShared.RequirementHandlers; +public class ExistingUserRequirementHandler : AuthorizationHandler +{ + private readonly IDbContextFactory _dbContextFactory; + private readonly ILogger _logger; + private readonly static ConcurrentDictionary _existingUserDict = []; + private readonly static ConcurrentDictionary _existingDiscordDict = []; + + public ExistingUserRequirementHandler(IDbContextFactory dbContext, ILogger logger) + { + _dbContextFactory = dbContext; + _logger = logger; + } + + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ExistingUserRequirement requirement) + { + try + { + var uid = context.User.Claims.SingleOrDefault(g => string.Equals(g.Type, LightlessClaimTypes.Uid, StringComparison.Ordinal))?.Value; + if (uid == null) + { + context.Fail(); + _logger.LogWarning("Failed to find UID in claims"); + return; + } + + var discordIdString = context.User.Claims.SingleOrDefault(g => string.Equals(g.Type, LightlessClaimTypes.DiscordId, StringComparison.Ordinal))?.Value; + if (discordIdString == null) + { + context.Fail(); + _logger.LogWarning("Failed to find DiscordId in claims"); + return; + } + if (!ulong.TryParse(discordIdString, out ulong discordId)) + { + _logger.LogWarning("Failed to parse DiscordId"); + context.Fail(); + return; + } + + using var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false); + + if (!_existingUserDict.TryGetValue(uid, out (bool Exists, DateTime LastCheck) existingUser) + || DateTime.UtcNow.Subtract(existingUser.LastCheck).TotalHours > 1) + { + var userExists = await dbContext.Users.SingleOrDefaultAsync(context => context.UID == uid).ConfigureAwait(false) != null; + _existingUserDict[uid] = existingUser = (userExists, DateTime.UtcNow); + } + if (!existingUser.Exists) + { + _logger.LogWarning("Failed to find Lightless User {User} in DB", uid); + context.Fail(); + return; + } + + if (!_existingDiscordDict.TryGetValue(discordId, out (bool Exists, DateTime LastCheck) existingDiscordUser) + || DateTime.UtcNow.Subtract(existingDiscordUser.LastCheck).TotalHours > 1) + { + var discordUserExists = await dbContext.LodeStoneAuth.AsNoTracking().SingleOrDefaultAsync(b => b.DiscordId == discordId).ConfigureAwait(false) != null; + _existingDiscordDict[discordId] = existingDiscordUser = (discordUserExists, DateTime.UtcNow); + } + + if (!existingDiscordUser.Exists) + { + _logger.LogWarning("Failed to find Discord User {User} in DB", discordId); + context.Fail(); + return; + } + + context.Succeed(requirement); + } + catch (Exception e) + { + _logger.LogWarning(e, "ExistingUserRequirementHandler failed"); + } + } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/RedisDbUserRequirementHandler.cs b/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/RedisDbUserRequirementHandler.cs new file mode 100644 index 0000000..4533c89 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/RedisDbUserRequirementHandler.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using LightlessSyncShared.Data; +using Microsoft.EntityFrameworkCore; +using LightlessSyncShared.Utils; +using StackExchange.Redis; +using Microsoft.Extensions.Logging; + +namespace LightlessSyncShared.RequirementHandlers; + +public class RedisDbUserRequirementHandler : AuthorizationHandler +{ + private readonly IDbContextFactory _dbContextFactory; + private readonly ILogger _logger; + private readonly IDatabase _redis; + + public RedisDbUserRequirementHandler(IDbContextFactory dbContextFactory, ILogger logger, IDatabase 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, LightlessClaimTypes.Uid, StringComparison.Ordinal))?.Value; + + if (uid == null) context.Fail(); + + if ((requirement.Requirements & UserRequirements.Identified) is UserRequirements.Identified) + { + var ident = await _redis.StringGetAsync("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/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/UserRequirement.cs b/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/UserRequirement.cs new file mode 100644 index 0000000..b0fcfe8 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/UserRequirement.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Authorization; + +namespace LightlessSyncShared.RequirementHandlers; + +public class UserRequirement : IAuthorizationRequirement +{ + public UserRequirement(UserRequirements requirements) + { + Requirements = requirements; + } + + public UserRequirements Requirements { get; } +} diff --git a/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/UserRequirementHandler.cs b/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/UserRequirementHandler.cs new file mode 100644 index 0000000..c438e01 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/UserRequirementHandler.cs @@ -0,0 +1,75 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using LightlessSyncShared.Data; +using Microsoft.EntityFrameworkCore; +using LightlessSyncShared.Utils; +using StackExchange.Redis; +using StackExchange.Redis.Extensions.Core.Abstractions; +using Microsoft.Extensions.Logging; + +namespace LightlessSyncShared.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, LightlessClaimTypes.Uid, StringComparison.Ordinal))?.Value; + + if (uid == null) + { + context.Fail(); + _logger.LogWarning("No user UID found in claims"); + return; + } + + if ((requirement.Requirements & UserRequirements.Identified) is UserRequirements.Identified) + { + var ident = await _redis.GetAsync("UID:" + uid).ConfigureAwait(false); + if (ident == RedisValue.EmptyString) + { + context.Fail(); + _logger.LogWarning("User {uid} not online", uid); + return; + } + } + + 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.LogWarning("Admin request for {uid} unauthenticated", uid); + return; + } + _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.LogWarning("Admin/Moderator for {uid} unauthenticated", uid); + return; + } + _logger.LogInformation("Admin/Moderator {uid} authenticated", uid); + } + + context.Succeed(requirement); + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/UserRequirements.cs b/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/UserRequirements.cs new file mode 100644 index 0000000..7fd9778 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/UserRequirements.cs @@ -0,0 +1,8 @@ +namespace LightlessSyncShared.RequirementHandlers; + +public enum UserRequirements +{ + Identified = 0b00000001, + Moderator = 0b00000010, + Administrator = 0b00000100, +} diff --git a/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/ValidTokenHubRequirementHandler.cs b/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/ValidTokenHubRequirementHandler.cs new file mode 100644 index 0000000..a54613d --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/ValidTokenHubRequirementHandler.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using LightlessSyncShared.Utils; +using System.Globalization; + +namespace LightlessSyncShared.RequirementHandlers; + +public class ValidTokenRequirementHandler : AuthorizationHandler +{ + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ValidTokenRequirement requirement) + { + var expirationClaimValue = context.User.Claims.SingleOrDefault(r => string.Equals(r.Type, LightlessClaimTypes.Expires, StringComparison.Ordinal)); + if (expirationClaimValue == null) + { + context.Fail(); + return Task.CompletedTask; + } + + DateTime expirationDate = new(long.Parse(expirationClaimValue.Value, CultureInfo.InvariantCulture), DateTimeKind.Utc); + if (expirationDate < DateTime.UtcNow) + { + context.Fail(); + return Task.CompletedTask; + } + + context.Succeed(requirement); + + return Task.CompletedTask; + } +} + +public class ValidTokenHubRequirementHandler : AuthorizationHandler +{ + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ValidTokenRequirement requirement, HubInvocationContext resource) + { + var expirationClaimValue = context.User.Claims.SingleOrDefault(r => string.Equals(r.Type, LightlessClaimTypes.Expires, StringComparison.Ordinal)); + if (expirationClaimValue == null) + { + context.Fail(); + return Task.CompletedTask; + } + + DateTime expirationDate = new(long.Parse(expirationClaimValue.Value, CultureInfo.InvariantCulture), DateTimeKind.Utc); + if (expirationDate < DateTime.UtcNow) + { + context.Fail(); + return Task.CompletedTask; + } + + context.Succeed(requirement); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/ValidTokenRequirement.cs b/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/ValidTokenRequirement.cs new file mode 100644 index 0000000..eeb2271 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/RequirementHandlers/ValidTokenRequirement.cs @@ -0,0 +1,5 @@ +using Microsoft.AspNetCore.Authorization; + +namespace LightlessSyncShared.RequirementHandlers; + +public class ValidTokenRequirement : IAuthorizationRequirement { } \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncShared/Services/IConfigurationService.cs b/LightlessSyncServer/LightlessSyncShared/Services/IConfigurationService.cs new file mode 100644 index 0000000..050728d --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Services/IConfigurationService.cs @@ -0,0 +1,14 @@ +using LightlessSyncShared.Utils.Configuration; + +namespace LightlessSyncShared.Services; + +public interface IConfigurationService where T : class, ILightlessConfiguration +{ + bool IsMain { get; } + + event EventHandler ConfigChangedEvent; + + T1 GetValue(string key); + T1 GetValueOrDefault(string key, T1 defaultValue); + string ToString(); +} diff --git a/LightlessSyncServer/LightlessSyncShared/Services/MareConfigurationController.cs b/LightlessSyncServer/LightlessSyncShared/Services/MareConfigurationController.cs new file mode 100644 index 0000000..ce430a6 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Services/MareConfigurationController.cs @@ -0,0 +1,60 @@ +using LightlessSyncShared.Utils.Configuration; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace LightlessSyncShared.Services; + +[Route("configuration/[controller]")] +[Authorize(Policy = "Internal")] +public class LightlessConfigurationController : Controller where T : class, ILightlessConfiguration +{ + private readonly ILogger> _logger; + private IOptionsMonitor _config; + + public LightlessConfigurationController(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 LightlessStaticFilesServerConfigurationController : LightlessConfigurationController +{ + public LightlessStaticFilesServerConfigurationController(IOptionsMonitor config, ILogger logger) : base(config, logger) + { + } +} + +public class LightlessBaseConfigurationController : LightlessConfigurationController +{ + public LightlessBaseConfigurationController(IOptionsMonitor config, ILogger logger) : base(config, logger) + { + } +} + +public class LightlessServerConfigurationController : LightlessConfigurationController +{ + public LightlessServerConfigurationController(IOptionsMonitor config, ILogger logger) : base(config, logger) + { + } +} + +public class LightlessServicesConfigurationController : LightlessConfigurationController +{ + public LightlessServicesConfigurationController(IOptionsMonitor config, ILogger logger) : base(config, logger) + { + } +} +#pragma warning restore MA0048 // File name must match type name diff --git a/LightlessSyncServer/LightlessSyncShared/Services/MareConfigurationServiceClient.cs b/LightlessSyncServer/LightlessSyncShared/Services/MareConfigurationServiceClient.cs new file mode 100644 index 0000000..bc2dd51 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Services/MareConfigurationServiceClient.cs @@ -0,0 +1,193 @@ +using LightlessSyncShared.Utils; +using LightlessSyncShared.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 LightlessSyncShared.Services; + +public class LightlessConfigurationServiceClient : IHostedService, IConfigurationService where T : class, ILightlessConfiguration +{ + 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; + public event EventHandler ConfigChangedEvent; + private IDisposable _onChanged; + + private Uri GetRoute(string key, string value) + { + if (_config.CurrentValue.GetType() == typeof(ServerConfiguration)) + return new Uri((_config.CurrentValue as ServerConfiguration).MainServerAddress, $"configuration/LightlessServerConfiguration/{nameof(LightlessServerConfigurationController.GetConfigurationEntry)}?key={key}&defaultValue={value}"); + if (_config.CurrentValue.GetType() == typeof(LightlessConfigurationBase)) + return new Uri((_config.CurrentValue as LightlessConfigurationBase).MainServerAddress, $"configuration/LightlessBaseConfiguration/{nameof(LightlessBaseConfigurationController.GetConfigurationEntry)}?key={key}&defaultValue={value}"); + if (_config.CurrentValue.GetType() == typeof(ServicesConfiguration)) + return new Uri((_config.CurrentValue as ServicesConfiguration).MainServerAddress, $"configuration/LightlessServicesConfiguration/{nameof(LightlessServicesConfigurationController.GetConfigurationEntry)}?key={key}&defaultValue={value}"); + if (_config.CurrentValue.GetType() == typeof(StaticFilesServerConfiguration)) + return new Uri((_config.CurrentValue as StaticFilesServerConfiguration).MainFileServerAddress, $"configuration/LightlessStaticFilesServerConfiguration/{nameof(LightlessStaticFilesServerConfigurationController.GetConfigurationEntry)}?key={key}&defaultValue={value}"); + + throw new NotSupportedException("Config is not supported to be gotten remotely"); + } + + public LightlessConfigurationServiceClient(ILogger> logger, IOptionsMonitor config, ServerTokenGenerator serverTokenGenerator) + { + _config = config; + _logger = logger; + _serverTokenGenerator = serverTokenGenerator; + _httpClient = new(); + _onChanged = config.OnChange((c) => { ConfigChangedEvent?.Invoke(this, EventArgs.Empty); }); + } + + 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 LightlessConfigurationServiceClient"); + _ = 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(); + _onChanged?.Dispose(); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncShared/Services/MareConfigurationServiceServer.cs b/LightlessSyncServer/LightlessSyncShared/Services/MareConfigurationServiceServer.cs new file mode 100644 index 0000000..c3118d6 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Services/MareConfigurationServiceServer.cs @@ -0,0 +1,67 @@ +using LightlessSyncShared.Utils; +using LightlessSyncShared.Utils.Configuration; +using Microsoft.Extensions.Options; +using System.Collections; +using System.Text; + +namespace LightlessSyncShared.Services; + +public sealed class LightlessConfigurationServiceServer : IDisposable, IConfigurationService where T : class, ILightlessConfiguration +{ + private readonly IOptionsMonitor _config; + private bool _disposed; + + public bool IsMain => true; + public event EventHandler ConfigChangedEvent; + private IDisposable _onChanged; + + public LightlessConfigurationServiceServer(IOptionsMonitor config) + { + _config = config; + _onChanged = config.OnChange((c) => { ConfigChangedEvent?.Invoke(this, EventArgs.Empty); }); + } + + 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(); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _onChanged.Dispose(); + _disposed = true; + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Utils/AllowedControllersFeatureProvider.cs b/LightlessSyncServer/LightlessSyncShared/Utils/AllowedControllersFeatureProvider.cs new file mode 100644 index 0000000..f787ebf --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Utils/AllowedControllersFeatureProvider.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Mvc.Controllers; +using System.Reflection; +using Microsoft.Extensions.Logging; + +namespace LightlessSyncShared.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/LightlessSyncServer/LightlessSyncShared/Utils/ClientMessage.cs b/LightlessSyncServer/LightlessSyncShared/Utils/ClientMessage.cs new file mode 100644 index 0000000..37d5bbf --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Utils/ClientMessage.cs @@ -0,0 +1,4 @@ +using LightlessSync.API.Data.Enum; + +namespace LightlessSyncShared.Utils; +public record ClientMessage(MessageSeverity Severity, string Message, string UID); diff --git a/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/AuthServiceConfiguration.cs b/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/AuthServiceConfiguration.cs new file mode 100644 index 0000000..5102022 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/AuthServiceConfiguration.cs @@ -0,0 +1,24 @@ +using System.Text; + +namespace LightlessSyncShared.Utils.Configuration; + +public class AuthServiceConfiguration : LightlessConfigurationBase +{ + 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 Uri PublicOAuthBaseUri { get; set; } = null; + public string? DiscordOAuthClientSecret { get; set; } = null; + public string? DiscordOAuthClientId { get; set; } = null; + public override string ToString() + { + StringBuilder sb = new(); + sb.AppendLine(base.ToString()); + sb.AppendLine($"{nameof(RedisPool)} => {RedisPool}"); + sb.AppendLine($"{nameof(GeoIPDbCityFile)} => {GeoIPDbCityFile}"); + sb.AppendLine($"{nameof(UseGeoIP)} => {UseGeoIP}"); + return sb.ToString(); + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/IMareConfiguration.cs b/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/IMareConfiguration.cs new file mode 100644 index 0000000..dd931d8 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/IMareConfiguration.cs @@ -0,0 +1,8 @@ +namespace LightlessSyncShared.Utils.Configuration; + +public interface ILightlessConfiguration +{ + T GetValueOrDefault(string key, T defaultValue); + T GetValue(string key); + string SerializeValue(string key, string defaultValue); +} diff --git a/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/MareConfigurationBase.cs b/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/MareConfigurationBase.cs new file mode 100644 index 0000000..4af8fe0 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/MareConfigurationBase.cs @@ -0,0 +1,51 @@ +using System.Reflection; +using System.Text; +using System.Text.Json; + +namespace LightlessSyncShared.Utils.Configuration; + +public class LightlessConfigurationBase : ILightlessConfiguration +{ + 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/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/ServerConfiguration.cs b/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/ServerConfiguration.cs new file mode 100644 index 0000000..27fbfde --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/ServerConfiguration.cs @@ -0,0 +1,52 @@ +using System.Text; + +namespace LightlessSyncShared.Utils.Configuration; + +public class ServerConfiguration : LightlessConfigurationBase +{ + [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; + + [RemoteConfiguration] + public int MaxCharaDataByUserVanity { get; set; } = 50; + public bool RunPermissionCleanupOnStartup { get; set; } = true; + public int HubExecutionConcurrencyFilter { get; set; } = 50; + + 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(RunPermissionCleanupOnStartup)} => {RunPermissionCleanupOnStartup}"); + sb.AppendLine($"{nameof(HubExecutionConcurrencyFilter)} => {HubExecutionConcurrencyFilter}"); + return sb.ToString(); + } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/ServicesConfiguration.cs b/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/ServicesConfiguration.cs new file mode 100644 index 0000000..46a173f --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/ServicesConfiguration.cs @@ -0,0 +1,34 @@ +using System.Text; + +namespace LightlessSyncShared.Utils.Configuration; + +public class ServicesConfiguration : LightlessConfigurationBase +{ + public string DiscordBotToken { get; set; } = string.Empty; + public ulong? DiscordChannelForMessages { get; set; } = null; + public ulong? DiscordChannelForCommands { get; set; } = null; + public ulong? DiscordRoleAprilFools2024 { get; set; } = null; + public ulong? DiscordChannelForBotLog { get; set; } = null!; + public ulong? DiscordRoleRegistered { get; set; } = null!; + public bool KickNonRegisteredUsers { get; set; } = false; + public Uri MainServerAddress { get; set; } = null; + public Dictionary VanityRoles { get; set; } = new Dictionary(); + + 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(DiscordChannelForCommands)} => {DiscordChannelForCommands}"); + sb.AppendLine($"{nameof(DiscordRoleAprilFools2024)} => {DiscordRoleAprilFools2024}"); + sb.AppendLine($"{nameof(DiscordRoleRegistered)} => {DiscordRoleRegistered}"); + sb.AppendLine($"{nameof(KickNonRegisteredUsers)} => {KickNonRegisteredUsers}"); + foreach (var role in VanityRoles) + { + sb.AppendLine($"{nameof(VanityRoles)} => {role.Key} = {role.Value}"); + } + return sb.ToString(); + } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/ShardConfiguration.cs b/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/ShardConfiguration.cs new file mode 100644 index 0000000..d5ecbf5 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/ShardConfiguration.cs @@ -0,0 +1,8 @@ +namespace LightlessSyncShared.Utils.Configuration; + +public class ShardConfiguration +{ + public List Continents { get; set; } + public string FileMatch { get; set; } + public Dictionary RegionUris { get; set; } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/StaticFilesServerConfiguration.cs b/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/StaticFilesServerConfiguration.cs new file mode 100644 index 0000000..85b9585 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/StaticFilesServerConfiguration.cs @@ -0,0 +1,45 @@ +using System.Text; + +namespace LightlessSyncShared.Utils.Configuration; + +public class StaticFilesServerConfiguration : LightlessConfigurationBase +{ + public bool IsDistributionNode { get; set; } = false; + public Uri MainFileServerAddress { get; set; } = null; + public Uri DistributionFileServerAddress { get; set; } = null; + public int ForcedDeletionOfFilesAfterHours { get; set; } = -1; + public double CacheSizeHardLimitInGiB { get; set; } = -1; + 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 ColdStorageUnusedFileRetentionPeriodInDays { get; set; } = 30; + [RemoteConfiguration] + public double SpeedTestHoursRateLimit { get; set; } = 0.5; + [RemoteConfiguration] + public Uri CdnFullUrl { get; set; } = null; + public ShardConfiguration? ShardConfiguration { get; set; } = null; + public override string ToString() + { + StringBuilder sb = new(); + sb.AppendLine(base.ToString()); + sb.AppendLine($"{nameof(MainFileServerAddress)} => {MainFileServerAddress}"); + 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(ColdStorageUnusedFileRetentionPeriodInDays)} => {ColdStorageUnusedFileRetentionPeriodInDays}"); + sb.AppendLine($"{nameof(UnusedFileRetentionPeriodInDays)} => {UnusedFileRetentionPeriodInDays}"); + sb.AppendLine($"{nameof(CacheDirectory)} => {CacheDirectory}"); + sb.AppendLine($"{nameof(DownloadQueueSize)} => {DownloadQueueSize}"); + sb.AppendLine($"{nameof(DownloadQueueReleaseSeconds)} => {DownloadQueueReleaseSeconds}"); + return sb.ToString(); + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Utils/IdBasedUserIdProvider.cs b/LightlessSyncServer/LightlessSyncShared/Utils/IdBasedUserIdProvider.cs new file mode 100644 index 0000000..834a6e9 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Utils/IdBasedUserIdProvider.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.SignalR; + +namespace LightlessSyncShared.Utils; + +public class IdBasedUserIdProvider : IUserIdProvider +{ + public string GetUserId(HubConnectionContext context) + { + return context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, LightlessClaimTypes.Uid, StringComparison.Ordinal))?.Value; + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Utils/MareClaimTypes.cs b/LightlessSyncServer/LightlessSyncShared/Utils/MareClaimTypes.cs new file mode 100644 index 0000000..f832cfa --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Utils/MareClaimTypes.cs @@ -0,0 +1,14 @@ +namespace LightlessSyncShared.Utils; + +public static class LightlessClaimTypes +{ + public const string Uid = "uid"; + public const string Alias = "alias"; + public const string CharaIdent = "character_identification"; + public const string Internal = "internal"; + public const string Expires = "expiration_date"; + public const string Continent = "continent"; + public const string DiscordUser = "discord_user"; + public const string DiscordId = "discord_user_id"; + public const string OAuthLoginToken = "oauth_login_token"; +} diff --git a/LightlessSyncServer/LightlessSyncShared/Utils/RemoteConfigurationAttribute.cs b/LightlessSyncServer/LightlessSyncShared/Utils/RemoteConfigurationAttribute.cs new file mode 100644 index 0000000..390e974 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Utils/RemoteConfigurationAttribute.cs @@ -0,0 +1,4 @@ +namespace LightlessSyncShared.Utils; + +[AttributeUsage(AttributeTargets.Property)] +public class RemoteConfigurationAttribute : Attribute { } \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncShared/Utils/ServerTokenGenerator.cs b/LightlessSyncServer/LightlessSyncShared/Utils/ServerTokenGenerator.cs new file mode 100644 index 0000000..bfa4d1a --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Utils/ServerTokenGenerator.cs @@ -0,0 +1,65 @@ +using LightlessSyncShared.Utils.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using System.Globalization; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace LightlessSyncShared.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(LightlessClaimTypes.Uid, _configuration.CurrentValue.ShardName), + new Claim(LightlessClaimTypes.Internal, "true"), + new Claim(LightlessClaimTypes.Expires, DateTime.Now.AddYears(1).Ticks.ToString(CultureInfo.InvariantCulture)) + }), + SigningCredentials = new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256Signature), + Expires = DateTime.Now.AddYears(1) + }; + + var handler = new JwtSecurityTokenHandler(); + var jwt = handler.CreateJwtSecurityToken(token); + var rawData = jwt.RawData; + + _tokenDictionary[signingKey] = rawData; + + _logger.LogInformation("Generated Token: {data}", rawData); + + return rawData; + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Utils/SharedDbFunctions.cs b/LightlessSyncServer/LightlessSyncShared/Utils/SharedDbFunctions.cs new file mode 100644 index 0000000..ae7ba57 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Utils/SharedDbFunctions.cs @@ -0,0 +1,129 @@ +using LightlessSyncShared.Data; +using LightlessSyncShared.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace LightlessSyncShared.Utils; + +public static class SharedDbFunctions +{ + public static async Task<(bool, string)> MigrateOrDeleteGroup(LightlessDbContext 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; + break; + } + } + + if (!groupHasMigrated) + { + context.GroupPairs.RemoveRange(groupPairs); + context.Groups.Remove(group); + } + + return (groupHasMigrated, newOwner); + } + + public static async Task PurgeUser(ILogger _logger, User user, LightlessDbContext 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 userFiles = dbContext.Files.Where(f => f.Uploaded && f.Uploader.UID == user.UID).ToList(); + dbContext.Files.RemoveRange(userFiles); + + 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); + } + + var defaultPermissions = await dbContext.UserDefaultPreferredPermissions.Where(u => u.UserUID == user.UID).ToListAsync().ConfigureAwait(false); + var groupPermissions = await dbContext.GroupPairPreferredPermissions.Where(u => u.UserUID == user.UID).ToListAsync().ConfigureAwait(false); + var individualPermissions = await dbContext.Permissions.Where(u => u.UserUID == user.UID || u.OtherUserUID == user.UID).ToListAsync().ConfigureAwait(false); + var bannedinGroups = await dbContext.GroupBans.Where(u => u.BannedUserUID == user.UID).ToListAsync().ConfigureAwait(false); + var hasBannedInGroups = await dbContext.GroupBans.Where(u => u.BannedByUID == user.UID).ToListAsync().ConfigureAwait(false); + + dbContext.GroupPairPreferredPermissions.RemoveRange(groupPermissions); + dbContext.UserDefaultPreferredPermissions.RemoveRange(defaultPermissions); + dbContext.Permissions.RemoveRange(individualPermissions); + dbContext.GroupBans.RemoveRange(bannedinGroups); + dbContext.GroupBans.RemoveRange(hasBannedInGroups); + + _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(LightlessDbContext 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/LightlessSyncServer/LightlessSyncShared/Utils/StringUtils.cs b/LightlessSyncServer/LightlessSyncShared/Utils/StringUtils.cs new file mode 100644 index 0000000..7e7c9a9 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Utils/StringUtils.cs @@ -0,0 +1,49 @@ +using System.Security.Cryptography; +using System.Text; + +namespace LightlessSyncShared.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/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/CacheController.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/CacheController.cs new file mode 100644 index 0000000..f2fc4a4 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/CacheController.cs @@ -0,0 +1,53 @@ +using LightlessSync.API.Routes; +using LightlessSyncStaticFilesServer.Services; +using LightlessSyncStaticFilesServer.Utils; +using Microsoft.AspNetCore.Mvc; + +namespace LightlessSyncStaticFilesServer.Controllers; + +[Route(LightlessFiles.Cache)] +public class CacheController : ControllerBase +{ + private readonly RequestFileStreamResultFactory _requestFileStreamResultFactory; + private readonly CachedFileProvider _cachedFileProvider; + private readonly RequestQueueService _requestQueue; + private readonly FileStatisticsService _fileStatisticsService; + + public CacheController(ILogger logger, RequestFileStreamResultFactory requestFileStreamResultFactory, + CachedFileProvider cachedFileProvider, RequestQueueService requestQueue, FileStatisticsService fileStatisticsService) : base(logger) + { + _requestFileStreamResultFactory = requestFileStreamResultFactory; + _cachedFileProvider = cachedFileProvider; + _requestQueue = requestQueue; + _fileStatisticsService = fileStatisticsService; + } + + [HttpGet(LightlessFiles.Cache_Get)] + public async Task GetFiles(Guid requestId) + { + _logger.LogDebug($"GetFile:{LightlessUser}:{requestId}"); + + if (!_requestQueue.IsActiveProcessing(requestId, LightlessUser, out var request)) return BadRequest(); + + _requestQueue.ActivateRequest(requestId); + + Response.ContentType = "application/octet-stream"; + + long requestSize = 0; + List substreams = new(); + + foreach (var fileHash in request.FileIds) + { + var fs = await _cachedFileProvider.DownloadAndGetLocalFileInfo(fileHash).ConfigureAwait(false); + if (fs == null) continue; + + substreams.Add(new(fs)); + + requestSize += fs.Length; + } + + _fileStatisticsService.LogRequest(requestSize); + + return _requestFileStreamResultFactory.Create(requestId, new BlockFileDataStream(substreams)); + } +} diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/ControllerBase.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/ControllerBase.cs new file mode 100644 index 0000000..c33fc16 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/ControllerBase.cs @@ -0,0 +1,18 @@ +using LightlessSyncShared.Utils; +using Microsoft.AspNetCore.Mvc; + +namespace LightlessSyncStaticFilesServer.Controllers; + +public class ControllerBase : Controller +{ + protected ILogger _logger; + + public ControllerBase(ILogger logger) + { + _logger = logger; + } + + protected string LightlessUser => HttpContext.User.Claims.First(f => string.Equals(f.Type, LightlessClaimTypes.Uid, StringComparison.Ordinal)).Value; + protected string Continent => HttpContext.User.Claims.FirstOrDefault(f => string.Equals(f.Type, LightlessClaimTypes.Continent, StringComparison.Ordinal))?.Value ?? "*"; + protected bool IsPriority => !string.IsNullOrEmpty(HttpContext.User.Claims.FirstOrDefault(f => string.Equals(f.Type, LightlessClaimTypes.Alias, StringComparison.Ordinal))?.Value ?? string.Empty); +} diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/DistributionController.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/DistributionController.cs new file mode 100644 index 0000000..3391f95 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/DistributionController.cs @@ -0,0 +1,29 @@ +using LightlessSync.API.Routes; +using LightlessSyncStaticFilesServer.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace LightlessSyncStaticFilesServer.Controllers; + +[Route(LightlessFiles.Distribution)] +public class DistributionController : ControllerBase +{ + private readonly CachedFileProvider _cachedFileProvider; + + public DistributionController(ILogger logger, CachedFileProvider cachedFileProvider) : base(logger) + { + _cachedFileProvider = cachedFileProvider; + } + + [HttpGet(LightlessFiles.Distribution_Get)] + [Authorize(Policy = "Internal")] + public async Task GetFile(string file) + { + _logger.LogInformation($"GetFile:{LightlessUser}:{file}"); + + var fs = await _cachedFileProvider.DownloadAndGetLocalFileInfo(file); + if (fs == null) return NotFound(); + + return PhysicalFile(fs.FullName, "application/octet-stream"); + } +} diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/MainController.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/MainController.cs new file mode 100644 index 0000000..2c8a2be --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/MainController.cs @@ -0,0 +1,66 @@ +using LightlessSync.API.Routes; +using LightlessSyncShared.Utils.Configuration; +using LightlessSyncStaticFilesServer.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace LightlessSyncStaticFilesServer.Controllers; + +[Route(LightlessFiles.Main)] +[Authorize(Policy = "Internal")] +public class MainController : ControllerBase +{ + private readonly IClientReadyMessageService _messageService; + private readonly MainServerShardRegistrationService _shardRegistrationService; + + public MainController(ILogger logger, IClientReadyMessageService lightlessHub, + MainServerShardRegistrationService shardRegistrationService) : base(logger) + { + _messageService = lightlessHub; + _shardRegistrationService = shardRegistrationService; + } + + [HttpGet(LightlessFiles.Main_SendReady)] + public async Task SendReadyToClients(string uid, Guid requestId) + { + await _messageService.SendDownloadReady(uid, requestId).ConfigureAwait(false); + return Ok(); + } + + [HttpPost("shardRegister")] + public IActionResult RegisterShard([FromBody] ShardConfiguration shardConfiguration) + { + try + { + _shardRegistrationService.RegisterShard(LightlessUser, shardConfiguration); + return Ok(); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Shard could not be registered {shard}", LightlessUser); + return BadRequest(); + } + } + + [HttpPost("shardUnregister")] + public IActionResult UnregisterShard() + { + _shardRegistrationService.UnregisterShard(LightlessUser); + return Ok(); + } + + [HttpPost("shardHeartbeat")] + public IActionResult ShardHeartbeat() + { + try + { + _shardRegistrationService.ShardHeartbeat(LightlessUser); + return Ok(); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Shard not registered: {shard}", LightlessUser); + return BadRequest(); + } + } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/RequestController.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/RequestController.cs new file mode 100644 index 0000000..999c269 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/RequestController.cs @@ -0,0 +1,63 @@ +using LightlessSync.API.Routes; +using LightlessSyncStaticFilesServer.Services; +using Microsoft.AspNetCore.Mvc; + +namespace LightlessSyncStaticFilesServer.Controllers; + +[Route(LightlessFiles.Request)] +public class RequestController : ControllerBase +{ + private readonly CachedFileProvider _cachedFileProvider; + private readonly RequestQueueService _requestQueue; + + public RequestController(ILogger logger, CachedFileProvider cachedFileProvider, RequestQueueService requestQueue) : base(logger) + { + _cachedFileProvider = cachedFileProvider; + _requestQueue = requestQueue; + } + + [HttpGet] + [Route(LightlessFiles.Request_Cancel)] + public async Task CancelQueueRequest(Guid requestId) + { + try + { + _requestQueue.RemoveFromQueue(requestId, LightlessUser, IsPriority); + return Ok(); + } + catch (OperationCanceledException) { return BadRequest(); } + } + + [HttpPost] + [Route(LightlessFiles.Request_Enqueue)] + public async Task PreRequestFilesAsync([FromBody] IEnumerable files) + { + try + { + foreach (var file in files) + { + _logger.LogDebug("Prerequested file: " + file); + await _cachedFileProvider.DownloadFileWhenRequired(file).ConfigureAwait(false); + } + + Guid g = Guid.NewGuid(); + await _requestQueue.EnqueueUser(new(g, LightlessUser, files.ToList()), IsPriority, HttpContext.RequestAborted); + + return Ok(g); + } + catch (OperationCanceledException) { return BadRequest(); } + } + + [HttpGet] + [Route(LightlessFiles.Request_Check)] + public async Task CheckQueueAsync(Guid requestId, [FromBody] IEnumerable files) + { + try + { + if (!_requestQueue.StillEnqueued(requestId, LightlessUser, IsPriority)) + await _requestQueue.EnqueueUser(new(requestId, LightlessUser, files.ToList()), IsPriority, HttpContext.RequestAborted); + return Ok(); + } + catch (OperationCanceledException) { return BadRequest(); } + } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/ServerFilesController.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/ServerFilesController.cs new file mode 100644 index 0000000..a81ca17 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/ServerFilesController.cs @@ -0,0 +1,363 @@ +using K4os.Compression.LZ4.Legacy; +using LightlessSync.API.Dto.Files; +using LightlessSync.API.Routes; +using LightlessSync.API.SignalR; +using LightlessSyncServer.Hubs; +using LightlessSyncShared.Data; +using LightlessSyncShared.Metrics; +using LightlessSyncShared.Models; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils.Configuration; +using LightlessSyncStaticFilesServer.Services; +using LightlessSyncStaticFilesServer.Utils; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using System.Collections.Concurrent; +using System.Security.Cryptography; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace LightlessSyncStaticFilesServer.Controllers; + +[Route(LightlessFiles.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 CachedFileProvider _cachedFileProvider; + private readonly IConfigurationService _configuration; + private readonly IHubContext _hubContext; + private readonly IDbContextFactory _lightlessDbContext; + private readonly LightlessMetrics _metricsClient; + private readonly MainServerShardRegistrationService _shardRegistrationService; + + public ServerFilesController(ILogger logger, CachedFileProvider cachedFileProvider, + IConfigurationService configuration, + IHubContext hubContext, + IDbContextFactory lightlessDbContext, LightlessMetrics metricsClient, + MainServerShardRegistrationService shardRegistrationService) : base(logger) + { + _basePath = configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseColdStorage), false) + ? configuration.GetValue(nameof(StaticFilesServerConfiguration.ColdStorageDirectory)) + : configuration.GetValue(nameof(StaticFilesServerConfiguration.CacheDirectory)); + _cachedFileProvider = cachedFileProvider; + _configuration = configuration; + _hubContext = hubContext; + _lightlessDbContext = lightlessDbContext; + _metricsClient = metricsClient; + _shardRegistrationService = shardRegistrationService; + } + + [HttpPost(LightlessFiles.ServerFiles_DeleteAll)] + public async Task FilesDeleteAll() + { + using var dbContext = await _lightlessDbContext.CreateDbContextAsync(); + var ownFiles = await dbContext.Files.Where(f => f.Uploaded && f.Uploader.UID == LightlessUser).ToListAsync().ConfigureAwait(false); + bool isColdStorage = _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseColdStorage), false); + + foreach (var dbFile in ownFiles) + { + var fi = FilePathUtil.GetFileInfoForHash(_basePath, dbFile.Hash); + if (fi != null) + { + _metricsClient.DecGauge(isColdStorage ? MetricsAPI.GaugeFilesTotalColdStorage : MetricsAPI.GaugeFilesTotal, fi == null ? 0 : 1); + _metricsClient.DecGauge(isColdStorage ? MetricsAPI.GaugeFilesTotalSizeColdStorage : MetricsAPI.GaugeFilesTotalSize, fi?.Length ?? 0); + + fi?.Delete(); + } + } + + dbContext.Files.RemoveRange(ownFiles); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + + return Ok(); + } + + [HttpGet(LightlessFiles.ServerFiles_GetSizes)] + public async Task FilesGetSizes([FromBody] List hashes) + { + using var dbContext = await _lightlessDbContext.CreateDbContextAsync(); + var forbiddenFiles = await dbContext.ForbiddenUploadEntries. + Where(f => hashes.Contains(f.Hash)).ToListAsync().ConfigureAwait(false); + List response = new(); + + var cacheFile = await dbContext.Files.AsNoTracking() + .Where(f => hashes.Contains(f.Hash)) + .Select(k => new { k.Hash, k.Size, k.RawSize }) + .ToListAsync().ConfigureAwait(false); + + var allFileShards = _shardRegistrationService.GetConfigurationsByContinent(Continent); + + foreach (var file in cacheFile) + { + var forbiddenFile = forbiddenFiles.SingleOrDefault(f => string.Equals(f.Hash, file.Hash, StringComparison.OrdinalIgnoreCase)); + Uri? baseUrl = null; + + if (forbiddenFile == null) + { + var matchingShards = allFileShards.Where(f => new Regex(f.FileMatch).IsMatch(file.Hash)).ToList(); + + var shard = matchingShards.SelectMany(g => g.RegionUris) + .OrderBy(g => Guid.NewGuid()).FirstOrDefault(); + + baseUrl = shard.Value ?? _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, + RawSize = file.RawSize + }); + } + + return Ok(JsonSerializer.Serialize(response)); + } + + [HttpGet(LightlessFiles.ServerFiles_DownloadServers)] + public async Task GetDownloadServers() + { + var allFileShards = _shardRegistrationService.GetConfigurationsByContinent(Continent); + return Ok(JsonSerializer.Serialize(allFileShards.SelectMany(t => t.RegionUris.Select(v => v.Value.ToString())))); + } + + [HttpPost(LightlessFiles.ServerFiles_FilesSend)] + public async Task FilesSend([FromBody] FilesSendDto filesSendDto) + { + using var dbContext = await _lightlessDbContext.CreateDbContextAsync(); + + 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 dbContext.ForbiddenUploadEntries.AsNoTracking().Where(f => userSentHashes.Contains(f.Hash)).AsNoTracking().ToDictionaryAsync(f => f.Hash, f => f).ConfigureAwait(false); + var existingFiles = await dbContext.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(ILightlessHub.Client_UserReceiveUploadStatus), new LightlessSync.API.Dto.User.UserDto(new(LightlessUser))) + .ConfigureAwait(false); + } + + return Ok(JsonSerializer.Serialize(notCoveredFiles.Values.ToList())); + } + + [HttpPost(LightlessFiles.ServerFiles_Upload + "/{hash}")] + [RequestSizeLimit(200 * 1024 * 1024)] + public async Task UploadFile(string hash, CancellationToken requestAborted) + { + using var dbContext = await _lightlessDbContext.CreateDbContextAsync(); + + _logger.LogInformation("{user}|{file}: Uploading", LightlessUser, hash); + + hash = hash.ToUpperInvariant(); + var existingFile = await dbContext.Files.SingleOrDefaultAsync(f => f.Hash == hash); + if (existingFile != null) return Ok(); + + SemaphoreSlim fileLock = await CreateFileLock(hash, requestAborted).ConfigureAwait(false); + + try + { + var existingFileCheck2 = await dbContext.Files.SingleOrDefaultAsync(f => f.Hash == hash); + if (existingFileCheck2 != null) + { + return Ok(); + } + + // copy the request body to memory + using var memoryStream = new MemoryStream(); + await Request.Body.CopyToAsync(memoryStream, requestAborted).ConfigureAwait(false); + + _logger.LogDebug("{user}|{file}: Finished uploading", LightlessUser, hash); + + await StoreData(hash, dbContext, memoryStream).ConfigureAwait(false); + + return Ok(); + } + catch (Exception e) + { + _logger.LogError(e, "{user}|{file}: Error during file upload", LightlessUser, hash); + return BadRequest(); + } + finally + { + try + { + fileLock.Release(); + fileLock.Dispose(); + } + catch (ObjectDisposedException) + { + // it's disposed whatever + } + finally + { + _fileUploadLocks.TryRemove(hash, out _); + } + } + } + + [HttpPost(LightlessFiles.ServerFiles_UploadMunged + "/{hash}")] + [RequestSizeLimit(200 * 1024 * 1024)] + public async Task UploadFileMunged(string hash, CancellationToken requestAborted) + { + using var dbContext = await _lightlessDbContext.CreateDbContextAsync(); + + _logger.LogInformation("{user}|{file}: Uploading munged", LightlessUser, hash); + hash = hash.ToUpperInvariant(); + var existingFile = await dbContext.Files.SingleOrDefaultAsync(f => f.Hash == hash); + if (existingFile != null) return Ok(); + + SemaphoreSlim fileLock = await CreateFileLock(hash, requestAborted).ConfigureAwait(false); + + try + { + var existingFileCheck2 = await dbContext.Files.SingleOrDefaultAsync(f => f.Hash == hash); + if (existingFileCheck2 != null) + { + return Ok(); + } + + // copy the request body to memory + using var compressedMungedStream = new MemoryStream(); + await Request.Body.CopyToAsync(compressedMungedStream, requestAborted).ConfigureAwait(false); + var unmungedFile = compressedMungedStream.ToArray(); + MungeBuffer(unmungedFile.AsSpan()); + await using MemoryStream unmungedMs = new(unmungedFile); + + _logger.LogDebug("{user}|{file}: Finished uploading, unmunged stream", LightlessUser, hash); + + await StoreData(hash, dbContext, unmungedMs); + + return Ok(); + } + catch (Exception e) + { + _logger.LogError(e, "{user}|{file}: Error during file upload", LightlessUser, hash); + return BadRequest(); + } + finally + { + try + { + fileLock.Release(); + fileLock.Dispose(); + } + catch (ObjectDisposedException) + { + // it's disposed whatever + } + finally + { + _fileUploadLocks.TryRemove(hash, out _); + } + } + } + + private async Task StoreData(string hash, LightlessDbContext dbContext, MemoryStream compressedFileStream) + { + var decompressedData = LZ4Wrapper.Unwrap(compressedFileStream.ToArray()); + // reset streams + compressedFileStream.Seek(0, SeekOrigin.Begin); + + // compute hash to verify + var hashString = BitConverter.ToString(SHA1.HashData(decompressedData)) + .Replace("-", "", StringComparison.Ordinal).ToUpperInvariant(); + if (!string.Equals(hashString, hash, StringComparison.Ordinal)) + throw new InvalidOperationException($"{LightlessUser}|{hash}: Hash does not match file, computed: {hashString}, expected: {hash}"); + + // save file + var path = FilePathUtil.GetFilePath(_basePath, hash); + using var fileStream = new FileStream(path, FileMode.Create); + await compressedFileStream.CopyToAsync(fileStream).ConfigureAwait(false); + _logger.LogDebug("{user}|{file}: Uploaded file saved to {path}", LightlessUser, hash, path); + + // update on db + await dbContext.Files.AddAsync(new FileCache() + { + Hash = hash, + UploadDate = DateTime.UtcNow, + UploaderUID = LightlessUser, + Size = compressedFileStream.Length, + Uploaded = true, + RawSize = decompressedData.LongLength + }).ConfigureAwait(false); + + await dbContext.SaveChangesAsync().ConfigureAwait(false); + + _logger.LogDebug("{user}|{file}: Uploaded file saved to DB", LightlessUser, hash); + + bool isColdStorage = _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseColdStorage), false); + + _metricsClient.IncGauge(isColdStorage ? MetricsAPI.GaugeFilesTotalColdStorage : MetricsAPI.GaugeFilesTotal, 1); + _metricsClient.IncGauge(isColdStorage ? MetricsAPI.GaugeFilesTotalSizeColdStorage : MetricsAPI.GaugeFilesTotalSize, compressedFileStream.Length); + } + + + private async Task CreateFileLock(string hash, CancellationToken requestAborted) + { + SemaphoreSlim? fileLock = null; + bool successfullyWaited = false; + while (!successfullyWaited && !requestAborted.IsCancellationRequested) + { + lock (_fileUploadLocks) + { + if (!_fileUploadLocks.TryGetValue(hash, out fileLock)) + { + _logger.LogDebug("{user}|{file}: Creating filelock", LightlessUser, hash); + _fileUploadLocks[hash] = fileLock = new SemaphoreSlim(1); + } + } + + try + { + _logger.LogDebug("{user}|{file}: Waiting for filelock", LightlessUser, hash); + await fileLock.WaitAsync(requestAborted).ConfigureAwait(false); + successfullyWaited = true; + } + catch (ObjectDisposedException) + { + _logger.LogWarning("{user}|{file}: Semaphore disposed, recreating", LightlessUser, hash); + } + } + + return fileLock; + } + + private static void MungeBuffer(Span buffer) + { + for (int i = 0; i < buffer.Length; ++i) + { + buffer[i] ^= 42; + } + } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/SpeedTestController.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/SpeedTestController.cs new file mode 100644 index 0000000..706e0a3 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/SpeedTestController.cs @@ -0,0 +1,61 @@ +using LightlessSync.API.Routes; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils; +using LightlessSyncShared.Utils.Configuration; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; + +namespace LightlessSyncStaticFilesServer.Controllers; + +[Route(LightlessFiles.Speedtest)] +public class SpeedTestController : ControllerBase +{ + private readonly IMemoryCache _memoryCache; + private readonly IConfigurationService _configurationService; + private const string RandomByteDataName = "SpeedTestRandomByteData"; + private static readonly SemaphoreSlim _speedtestSemaphore = new(10, 10); + + public SpeedTestController(ILogger logger, IMemoryCache memoryCache, + IConfigurationService configurationService) : base(logger) + { + _memoryCache = memoryCache; + _configurationService = configurationService; + } + + [HttpGet(LightlessFiles.Speedtest_Run)] + public async Task DownloadTest(CancellationToken cancellationToken) + { + var user = HttpContext.User.Claims.First(f => string.Equals(f.Type, LightlessClaimTypes.Uid, StringComparison.Ordinal)).Value; + var speedtestLimit = _configurationService.GetValueOrDefault(nameof(StaticFilesServerConfiguration.SpeedTestHoursRateLimit), 0.5); + if (_memoryCache.TryGetValue(user, out var value)) + { + var hoursRemaining = value.Subtract(DateTime.UtcNow).TotalHours; + return StatusCode(429, $"Can perform speedtest every {speedtestLimit} hours. {hoursRemaining:F2} hours remain."); + } + + await _speedtestSemaphore.WaitAsync(cancellationToken); + + try + { + var expiry = DateTime.UtcNow.Add(TimeSpan.FromHours(speedtestLimit)); + _memoryCache.Set(user, expiry, TimeSpan.FromHours(speedtestLimit)); + + var randomByteData = _memoryCache.GetOrCreate(RandomByteDataName, (entry) => + { + byte[] data = new byte[100 * 1024 * 1024]; + new Random().NextBytes(data); + return data; + }); + + return File(randomByteData, "application/octet-stream", "speedtest.dat"); + } + catch (OperationCanceledException) + { + return StatusCode(499, "Cancelled"); + } + finally + { + _speedtestSemaphore.Release(); + } + } +} diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/DummyHub.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/DummyHub.cs new file mode 100644 index 0000000..de7b656 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/DummyHub.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.SignalR; + +// this is a very hacky way to attach this file server to the main lightless 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 LightlessSyncServer.Hubs; +public class LightlessHub : 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/LightlessSyncServer/LightlessSyncStaticFilesServer/LightlessSyncStaticFilesServer.csproj b/LightlessSyncServer/LightlessSyncStaticFilesServer/LightlessSyncStaticFilesServer.csproj new file mode 100644 index 0000000..0676329 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/LightlessSyncStaticFilesServer.csproj @@ -0,0 +1,38 @@ + + + + net9.0 + enable + + + + + + + + + + + Never + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Program.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Program.cs new file mode 100644 index 0000000..ac8e764 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Program.cs @@ -0,0 +1,63 @@ +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils.Configuration; + +namespace LightlessSyncStaticFilesServer; + +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 LightlessSync Static Files Server Configuration (IsMain: {isMain})", options.IsMain); + logger.LogInformation(options.ToString()); + logger.LogInformation("Loaded LightlessSync 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() + .UseConsoleLifetime() + .ConfigureAppConfiguration((ctx, config) => + { + var appSettingsPath = Environment.GetEnvironmentVariable("APPSETTINGS_PATH"); + if (!string.IsNullOrEmpty(appSettingsPath)) + { + config.AddJsonFile(appSettingsPath, optional: true, reloadOnChange: true); + } + else + { + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); + } + + config.AddEnvironmentVariables(); + }) + .ConfigureLogging((ctx, builder) => + { + builder.AddConfiguration(ctx.Configuration.GetSection("Logging")); + builder.AddFile(o => o.RootPath = AppContext.BaseDirectory); + }) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseContentRoot(AppContext.BaseDirectory); + webBuilder.UseStartup(ctx => new Startup(ctx.Configuration, logger)); + }); + } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Properties/launchSettings.json b/LightlessSyncServer/LightlessSyncStaticFilesServer/Properties/launchSettings.json new file mode 100644 index 0000000..19290d9 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:21378", + "sslPort": 44331 + } + }, + "profiles": { + "LightlessSyncStaticFilesServer": { + "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/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/CachedFileProvider.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/CachedFileProvider.cs new file mode 100644 index 0000000..d63c001 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/CachedFileProvider.cs @@ -0,0 +1,214 @@ +using LightlessSyncShared.Metrics; +using LightlessSyncShared.Services; +using LightlessSyncStaticFilesServer.Utils; +using System.Collections.Concurrent; +using System.Net.Http.Headers; +using LightlessSyncShared.Utils; +using LightlessSync.API.Routes; +using LightlessSyncShared.Utils.Configuration; + +namespace LightlessSyncStaticFilesServer.Services; + +public sealed class CachedFileProvider : IDisposable +{ + private readonly IConfigurationService _configuration; + private readonly ILogger _logger; + private readonly FileStatisticsService _fileStatisticsService; + private readonly LightlessMetrics _metrics; + private readonly ServerTokenGenerator _generator; + private readonly Uri _remoteCacheSourceUri; + private readonly string _hotStoragePath; + 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, LightlessMetrics metrics, ServerTokenGenerator generator) + { + _configuration = configuration; + _logger = logger; + _fileStatisticsService = fileStatisticsService; + _metrics = metrics; + _generator = generator; + _remoteCacheSourceUri = configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.DistributionFileServerAddress), null); + _isDistributionServer = configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.IsDistributionNode), false); + _hotStoragePath = configuration.GetValue(nameof(StaticFilesServerConfiguration.CacheDirectory)); + _httpClient = new(); + _httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("LightlessSyncServer", "1.0.0.0")); + _httpClient.Timeout = TimeSpan.FromSeconds(300); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _httpClient?.Dispose(); + } + + private async Task DownloadTask(string hash) + { + var destinationFilePath = FilePathUtil.GetFilePath(_hotStoragePath, hash); + + // first check cold storage + if (TryCopyFromColdStorage(hash, destinationFilePath)) return; + + // if cold storage is not configured or file not found or error is present try to download file from remote + var downloadUrl = LightlessFiles.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); + 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 = response.Content.Headers.ContentLength > 1024 * 1024 ? 4096 : 1024; + 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(MetricsAPI.GaugeFilesTotal); + _metrics.IncGauge(MetricsAPI.GaugeFilesTotalSize, FilePathUtil.GetFileInfoForHash(_hotStoragePath, hash).Length); + response.Dispose(); + } + + private bool TryCopyFromColdStorage(string hash, string destinationFilePath) + { + if (!_configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseColdStorage), false)) return false; + + string coldStorageDir = _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.ColdStorageDirectory), string.Empty); + if (string.IsNullOrEmpty(coldStorageDir)) return false; + + var coldStorageFilePath = FilePathUtil.GetFileInfoForHash(coldStorageDir, hash); + if (coldStorageFilePath == null) return false; + + try + { + _logger.LogDebug("Copying {hash} from cold storage: {path}", hash, coldStorageFilePath); + var tempFileName = destinationFilePath + ".dl"; + File.Copy(coldStorageFilePath.FullName, tempFileName, true); + File.Move(tempFileName, destinationFilePath, true); + coldStorageFilePath.LastAccessTimeUtc = DateTime.UtcNow; + var destinationFile = new FileInfo(destinationFilePath); + destinationFile.LastAccessTimeUtc = DateTime.UtcNow; + destinationFile.CreationTimeUtc = DateTime.UtcNow; + destinationFile.LastWriteTimeUtc = DateTime.UtcNow; + _metrics.IncGauge(MetricsAPI.GaugeFilesTotal); + _metrics.IncGauge(MetricsAPI.GaugeFilesTotalSize, new FileInfo(destinationFilePath).Length); + return true; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Could not copy {coldStoragePath} from cold storage", coldStorageFilePath); + } + + return false; + } + + public async Task DownloadFileWhenRequired(string hash) + { + var fi = FilePathUtil.GetFileInfoForHash(_hotStoragePath, hash); + if (fi == null && IsMainServer) + { + TryCopyFromColdStorage(hash, FilePathUtil.GetFilePath(_hotStoragePath, hash)); + return; + } + + await _downloadSemaphore.WaitAsync().ConfigureAwait(false); + if ((fi == null || (fi?.Length ?? 0) == 0) + && (!_currentTransfers.TryGetValue(hash, out var downloadTask) + || (downloadTask?.IsCompleted ?? true))) + { + _currentTransfers[hash] = Task.Run(async () => + { + try + { + _metrics.IncGauge(MetricsAPI.GaugeFilesDownloadingFromCache); + await DownloadTask(hash).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error during Download Task for {hash}", hash); + } + finally + { + _metrics.DecGauge(MetricsAPI.GaugeFilesDownloadingFromCache); + _currentTransfers.Remove(hash, out _); + } + }); + } + _downloadSemaphore.Release(); + } + + public FileInfo? GetLocalFilePath(string hash) + { + var fi = FilePathUtil.GetFileInfoForHash(_hotStoragePath, hash); + if (fi == null) return null; + + fi.LastAccessTimeUtc = DateTime.UtcNow; + + _fileStatisticsService.LogFile(hash, fi.Length); + + return new FileInfo(fi.FullName); + } + + public async Task DownloadAndGetLocalFileInfo(string hash) + { + await DownloadFileWhenRequired(hash).ConfigureAwait(false); + + if (_currentTransfers.TryGetValue(hash, out var downloadTask)) + { + try + { + using CancellationTokenSource cts = new(); + cts.CancelAfter(TimeSpan.FromSeconds(300)); + _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); + } + } + + return GetLocalFilePath(hash); + } + + public bool AnyFilesDownloading(List hashes) + { + return hashes.Exists(_currentTransfers.Keys.Contains); + } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/FileStatisticsService.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/FileStatisticsService.cs new file mode 100644 index 0000000..7a30138 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/FileStatisticsService.cs @@ -0,0 +1,94 @@ +using LightlessSyncShared.Metrics; +using System.Collections.Concurrent; + +namespace LightlessSyncStaticFilesServer.Services; + +public class FileStatisticsService : IHostedService +{ + private readonly LightlessMetrics _metrics; + private readonly ILogger _logger; + private CancellationTokenSource _resetCancellationTokenSource; + private ConcurrentDictionary _pastHourFiles = new(StringComparer.Ordinal); + private ConcurrentDictionary _pastDayFiles = new(StringComparer.Ordinal); + + public FileStatisticsService(LightlessMetrics 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/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/IClientReadyMessageService.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/IClientReadyMessageService.cs new file mode 100644 index 0000000..21c5ae7 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/IClientReadyMessageService.cs @@ -0,0 +1,6 @@ +namespace LightlessSyncStaticFilesServer.Services; + +public interface IClientReadyMessageService +{ + Task SendDownloadReady(string uid, Guid requestId); +} diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/MainClientReadyMessageService.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/MainClientReadyMessageService.cs new file mode 100644 index 0000000..ec39786 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/MainClientReadyMessageService.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.SignalR; +using LightlessSync.API.SignalR; +using LightlessSyncServer.Hubs; + +namespace LightlessSyncStaticFilesServer.Services; + +public class MainClientReadyMessageService : IClientReadyMessageService +{ + private readonly ILogger _logger; + private readonly IHubContext _lightlessHub; + + public MainClientReadyMessageService(ILogger logger, IHubContext lightlessHub) + { + _logger = logger; + _lightlessHub = lightlessHub; + } + + public async Task SendDownloadReady(string uid, Guid requestId) + { + _logger.LogInformation("Sending Client Ready for {uid}:{requestId} to SignalR", uid, requestId); + await _lightlessHub.Clients.User(uid).SendAsync(nameof(ILightlessHub.Client_DownloadReady), requestId).ConfigureAwait(false); + } +} diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/MainFileCleanupService.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/MainFileCleanupService.cs new file mode 100644 index 0000000..c0bcbe0 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/MainFileCleanupService.cs @@ -0,0 +1,367 @@ +using ByteSizeLib; +using K4os.Compression.LZ4.Legacy; +using LightlessSyncShared.Data; +using LightlessSyncShared.Metrics; +using LightlessSyncShared.Models; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils.Configuration; +using LightlessSyncStaticFilesServer.Utils; +using Microsoft.EntityFrameworkCore; + +namespace LightlessSyncStaticFilesServer.Services; + +public class MainFileCleanupService : IHostedService +{ + private readonly IConfigurationService _configuration; + private readonly IDbContextFactory _dbContextFactory; + private readonly ILogger _logger; + private readonly LightlessMetrics _metrics; + private CancellationTokenSource _cleanupCts; + + public MainFileCleanupService(LightlessMetrics metrics, ILogger logger, + IConfigurationService configuration, + IDbContextFactory dbContextFactory) + { + _metrics = metrics; + _logger = logger; + _configuration = configuration; + _dbContextFactory = dbContextFactory; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Cleanup Service started"); + + _cleanupCts = new(); + + _ = Task.Run(() => CleanUpTask(_cleanupCts.Token)).ConfigureAwait(false); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _cleanupCts.Cancel(); + + return Task.CompletedTask; + } + + private List CleanUpFilesBeyondSizeLimit(List files, double sizeLimit, bool deleteFromDb, LightlessDbContext dbContext, CancellationToken ct) + { + if (sizeLimit <= 0) + { + return []; + } + + try + { + _logger.LogInformation("Cleaning up files beyond the cache size limit of {cacheSizeLimit} GiB", sizeLimit); + var allLocalFiles = files + .OrderBy(f => f.LastAccessTimeUtc).ToList(); + var totalCacheSizeInBytes = allLocalFiles.Sum(s => s.Length); + long cacheSizeLimitInBytes = (long)ByteSize.FromGibiBytes(sizeLimit).Bytes; + while (totalCacheSizeInBytes > cacheSizeLimitInBytes && allLocalFiles.Count != 0 && !ct.IsCancellationRequested) + { + var oldestFile = allLocalFiles[0]; + allLocalFiles.RemoveAt(0); + totalCacheSizeInBytes -= oldestFile.Length; + _logger.LogInformation("Deleting {oldestFile} with size {size}MiB", oldestFile.FullName, ByteSize.FromBytes(oldestFile.Length).MebiBytes); + oldestFile.Delete(); + FileCache f = new() { Hash = oldestFile.Name.ToUpperInvariant() }; + if (deleteFromDb) + dbContext.Entry(f).State = EntityState.Deleted; + } + + return allLocalFiles; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error during cache size limit cleanup"); + } + + return []; + } + + private List CleanUpOrphanedFiles(List allFiles, List allPhysicalFiles, CancellationToken ct) + { + var allFilesHashes = new HashSet(allFiles.Select(a => a.Hash.ToUpperInvariant()), StringComparer.Ordinal); + foreach (var file in allPhysicalFiles.ToList()) + { + if (!allFilesHashes.Contains(file.Name.ToUpperInvariant())) + { + _metrics.DecGauge(MetricsAPI.GaugeFilesTotalSize, file.Length); + _metrics.DecGauge(MetricsAPI.GaugeFilesTotal); + file.Delete(); + _logger.LogInformation("File not in DB, deleting: {fileName}", file.Name); + allPhysicalFiles.Remove(file); + } + + ct.ThrowIfCancellationRequested(); + } + + return allPhysicalFiles; + } + + private async Task> CleanUpOutdatedFiles(string dir, List allFilesInDir, int unusedRetention, int forcedDeletionAfterHours, + bool deleteFromDb, LightlessDbContext dbContext, CancellationToken ct) + { + 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); + } + + // clean up files in DB but not on disk or last access is expired + var prevTime = DateTime.Now.Subtract(TimeSpan.FromDays(unusedRetention)); + var prevTimeForcedDeletion = DateTime.Now.Subtract(TimeSpan.FromHours(forcedDeletionAfterHours)); + List allDbFiles = await dbContext.Files.ToListAsync(ct).ConfigureAwait(false); + List removedFileHashes; + + if (!deleteFromDb) + { + removedFileHashes = CleanupViaFiles(allFilesInDir, forcedDeletionAfterHours, prevTime, prevTimeForcedDeletion, ct); + } + else + { + removedFileHashes = await CleanupViaDb(dir, forcedDeletionAfterHours, dbContext, prevTime, prevTimeForcedDeletion, allDbFiles, ct).ConfigureAwait(false); + } + + // clean up files that are on disk but not in DB anymore + return CleanUpOrphanedFiles(allDbFiles, allFilesInDir.Where(c => !removedFileHashes.Contains(c.Name, StringComparer.OrdinalIgnoreCase)).ToList(), ct); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error during file cleanup of old files"); + } + + return []; + } + + private void CleanUpStuckUploads(LightlessDbContext dbContext) + { + var pastTime = DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(20)); + var stuckUploads = dbContext.Files.Where(f => !f.Uploaded && f.UploadDate < pastTime); + dbContext.Files.RemoveRange(stuckUploads); + } + + private async Task CleanUpTask(CancellationToken ct) + { + InitializeGauges(); + + while (!ct.IsCancellationRequested) + { + var cleanupCheckMinutes = _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.CleanupCheckInMinutes), 15); + bool useColdStorage = _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseColdStorage), false); + var hotStorageDir = _configuration.GetValue(nameof(StaticFilesServerConfiguration.CacheDirectory)); + var coldStorageDir = _configuration.GetValue(nameof(StaticFilesServerConfiguration.ColdStorageDirectory)); + using var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false); + + _logger.LogInformation("Running File Cleanup Task"); + + try + { + using CancellationTokenSource timedCts = new(); + timedCts.CancelAfter(TimeSpan.FromMinutes(cleanupCheckMinutes - 1)); + using var linkedTokenCts = CancellationTokenSource.CreateLinkedTokenSource(timedCts.Token, ct); + var linkedToken = linkedTokenCts.Token; + + DirectoryInfo dirHotStorage = new(hotStorageDir); + _logger.LogInformation("File Cleanup Task gathering hot storage files"); + var allFilesInHotStorage = dirHotStorage.GetFiles("*", SearchOption.AllDirectories).ToList(); + + var unusedRetention = _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UnusedFileRetentionPeriodInDays), 14); + var forcedDeletionAfterHours = _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.ForcedDeletionOfFilesAfterHours), -1); + var sizeLimit = _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.CacheSizeHardLimitInGiB), -1); + + _logger.LogInformation("File Cleanup Task cleaning up outdated hot storage files"); + var remainingHotFiles = await CleanUpOutdatedFiles(hotStorageDir, allFilesInHotStorage, unusedRetention, forcedDeletionAfterHours, + deleteFromDb: !useColdStorage, dbContext: dbContext, + ct: linkedToken).ConfigureAwait(false); + + _logger.LogInformation("File Cleanup Task cleaning up hot storage file beyond size limit"); + var finalRemainingHotFiles = CleanUpFilesBeyondSizeLimit(remainingHotFiles, sizeLimit, + deleteFromDb: !useColdStorage, dbContext: dbContext, + ct: linkedToken); + + if (useColdStorage) + { + DirectoryInfo dirColdStorage = new(coldStorageDir); + _logger.LogInformation("File Cleanup Task gathering cold storage files"); + var allFilesInColdStorageDir = dirColdStorage.GetFiles("*", SearchOption.AllDirectories).ToList(); + + var coldStorageRetention = _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.ColdStorageUnusedFileRetentionPeriodInDays), 60); + var coldStorageSize = _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.ColdStorageSizeHardLimitInGiB), -1); + + // clean up cold storage + _logger.LogInformation("File Cleanup Task cleaning up outdated cold storage files"); + var remainingColdFiles = await CleanUpOutdatedFiles(coldStorageDir, allFilesInColdStorageDir, coldStorageRetention, forcedDeletionAfterHours: -1, + deleteFromDb: true, dbContext: dbContext, + ct: linkedToken).ConfigureAwait(false); + _logger.LogInformation("File Cleanup Task cleaning up cold storage file beyond size limit"); + var finalRemainingColdFiles = CleanUpFilesBeyondSizeLimit(remainingColdFiles, coldStorageSize, + deleteFromDb: true, dbContext: dbContext, + ct: linkedToken); + } + } + catch (Exception e) + { + _logger.LogError(e, "Error during cleanup task"); + } + finally + { + CleanUpStuckUploads(dbContext); + + await dbContext.SaveChangesAsync(ct).ConfigureAwait(false); + } + + if (useColdStorage) + { + DirectoryInfo dirColdStorageAfterCleanup = new(coldStorageDir); + var allFilesInColdStorageAfterCleanup = dirColdStorageAfterCleanup.GetFiles("*", SearchOption.AllDirectories).ToList(); + + _metrics.SetGaugeTo(MetricsAPI.GaugeFilesTotalSizeColdStorage, allFilesInColdStorageAfterCleanup.Sum(f => { try { return f.Length; } catch { return 0; } })); + _metrics.SetGaugeTo(MetricsAPI.GaugeFilesTotalColdStorage, allFilesInColdStorageAfterCleanup.Count); + } + + DirectoryInfo dirHotStorageAfterCleanup = new(hotStorageDir); + var allFilesInHotStorageAfterCleanup = dirHotStorageAfterCleanup.GetFiles("*", SearchOption.AllDirectories).ToList(); + + _metrics.SetGaugeTo(MetricsAPI.GaugeFilesTotalSize, allFilesInHotStorageAfterCleanup.Sum(f => { try { return f.Length; } catch { return 0; } })); + _metrics.SetGaugeTo(MetricsAPI.GaugeFilesTotal, allFilesInHotStorageAfterCleanup.Count); + + 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 async Task> CleanupViaDb(string dir, int forcedDeletionAfterHours, + LightlessDbContext dbContext, DateTime lastAccessCutoffTime, DateTime forcedDeletionCutoffTime, List allDbFiles, CancellationToken ct) + { + int fileCounter = 0; + List removedFileHashes = new(); + foreach (var fileCache in allDbFiles.Where(f => f.Uploaded)) + { + bool deleteCurrentFile = false; + var file = FilePathUtil.GetFileInfoForHash(dir, fileCache.Hash); + if (file == null) + { + _logger.LogInformation("File does not exist anymore: {fileName}", fileCache.Hash); + deleteCurrentFile = true; + } + else if (file != null && file.LastAccessTime < lastAccessCutoffTime) + { + _logger.LogInformation("File outdated: {fileName}, {fileSize}MiB", file.Name, ByteSize.FromBytes(file.Length).MebiBytes); + deleteCurrentFile = true; + } + else if (file != null && forcedDeletionAfterHours > 0 && file.LastWriteTime < forcedDeletionCutoffTime) + { + _logger.LogInformation("File forcefully deleted: {fileName}, {fileSize}MiB", file.Name, ByteSize.FromBytes(file.Length).MebiBytes); + deleteCurrentFile = true; + } + + // only used if file in db has no raw size for whatever reason + if (!deleteCurrentFile && file != null && fileCache.RawSize == 0) + { + try + { + var length = LZ4Wrapper.Unwrap(File.ReadAllBytes(file.FullName)).LongLength; + _logger.LogInformation("Setting Raw File Size of " + fileCache.Hash + " to " + length); + fileCache.RawSize = length; + if (fileCounter % 1000 == 0) + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not unpack {fileName}", file.FullName); + } + } + + // do actual deletion of file and remove also from db if needed + if (deleteCurrentFile) + { + if (file != null) file.Delete(); + + removedFileHashes.Add(fileCache.Hash); + + dbContext.Files.Remove(fileCache); + } + + // only used if file in db has no size for whatever reason + if (!deleteCurrentFile && file != null && fileCache.Size == 0) + { + _logger.LogInformation("Setting File Size of " + fileCache.Hash + " to " + file.Length); + fileCache.Size = file.Length; + // commit every 1000 files to db + if (fileCounter % 1000 == 0) + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + + fileCounter++; + + ct.ThrowIfCancellationRequested(); + } + + return removedFileHashes; + } + + private List CleanupViaFiles(List allFilesInDir, int forcedDeletionAfterHours, + DateTime lastAccessCutoffTime, DateTime forcedDeletionCutoffTime, CancellationToken ct) + { + List removedFileHashes = new List(); + + foreach (var file in allFilesInDir) + { + bool deleteCurrentFile = false; + if (file != null && file.LastAccessTime < lastAccessCutoffTime) + { + _logger.LogInformation("File outdated: {fileName}, {fileSize}MiB", file.Name, ByteSize.FromBytes(file.Length).MebiBytes); + deleteCurrentFile = true; + } + else if (file != null && forcedDeletionAfterHours > 0 && file.LastWriteTime < forcedDeletionCutoffTime) + { + _logger.LogInformation("File forcefully deleted: {fileName}, {fileSize}MiB", file.Name, ByteSize.FromBytes(file.Length).MebiBytes); + deleteCurrentFile = true; + } + + if (deleteCurrentFile) + { + if (file != null) file.Delete(); + + removedFileHashes.Add(file.Name); + } + + ct.ThrowIfCancellationRequested(); + } + + return removedFileHashes; + } + + private void InitializeGauges() + { + bool useColdStorage = _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseColdStorage), false); + + if (useColdStorage) + { + var coldStorageDir = _configuration.GetValue(nameof(StaticFilesServerConfiguration.ColdStorageDirectory)); + + DirectoryInfo dirColdStorage = new(coldStorageDir); + var allFilesInColdStorageDir = dirColdStorage.GetFiles("*", SearchOption.AllDirectories).ToList(); + + _metrics.SetGaugeTo(MetricsAPI.GaugeFilesTotalSizeColdStorage, allFilesInColdStorageDir.Sum(f => f.Length)); + _metrics.SetGaugeTo(MetricsAPI.GaugeFilesTotalColdStorage, allFilesInColdStorageDir.Count); + } + + var hotStorageDir = _configuration.GetValue(nameof(StaticFilesServerConfiguration.CacheDirectory)); + DirectoryInfo dirHotStorage = new(hotStorageDir); + var allFilesInHotStorage = dirHotStorage.GetFiles("*", SearchOption.AllDirectories).ToList(); + + _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/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/MainServerShardRegistrationService.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/MainServerShardRegistrationService.cs new file mode 100644 index 0000000..7f31c29 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/MainServerShardRegistrationService.cs @@ -0,0 +1,96 @@ +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils.Configuration; +using System.Collections.Concurrent; +using System.Collections.Frozen; + +namespace LightlessSyncStaticFilesServer.Services; + +public class MainServerShardRegistrationService : IHostedService +{ + private readonly ILogger _logger; + private readonly IConfigurationService _configurationService; + private readonly ConcurrentDictionary _shardConfigs = new(StringComparer.Ordinal); + private readonly ConcurrentDictionary _shardHeartbeats = new(StringComparer.Ordinal); + private readonly CancellationTokenSource _periodicCheckCts = new(); + + public MainServerShardRegistrationService(ILogger logger, + IConfigurationService configurationService) + { + _logger = logger; + _configurationService = configurationService; + } + + public void RegisterShard(string shardName, ShardConfiguration shardConfiguration) + { + if (shardConfiguration == null || shardConfiguration == default) + throw new InvalidOperationException("Empty configuration provided"); + + if (_shardConfigs.ContainsKey(shardName)) + _logger.LogInformation("Re-Registering Shard {name}", shardName); + else + _logger.LogInformation("Registering Shard {name}", shardName); + + _shardHeartbeats[shardName] = DateTime.UtcNow; + _shardConfigs[shardName] = shardConfiguration; + } + + public void UnregisterShard(string shardName) + { + _logger.LogInformation("Unregistering Shard {name}", shardName); + + _shardHeartbeats.TryRemove(shardName, out _); + _shardConfigs.TryRemove(shardName, out _); + } + + public List GetConfigurationsByContinent(string continent) + { + var shardConfigs = _shardConfigs.Values.Where(v => v.Continents.Contains(continent, StringComparer.OrdinalIgnoreCase)).ToList(); + if (shardConfigs.Any()) return shardConfigs; + shardConfigs = _shardConfigs.Values.Where(v => v.Continents.Contains("*", StringComparer.OrdinalIgnoreCase)).ToList(); + if (shardConfigs.Any()) return shardConfigs; + return [new ShardConfiguration() { + Continents = ["*"], + FileMatch = ".*", + RegionUris = new(StringComparer.Ordinal) { + { "Central", _configurationService.GetValue(nameof(StaticFilesServerConfiguration.CdnFullUrl)) } + } }]; + } + + public void ShardHeartbeat(string shardName) + { + if (!_shardConfigs.ContainsKey(shardName)) + throw new InvalidOperationException("Shard not registered"); + + _logger.LogInformation("Heartbeat from {name}", shardName); + _shardHeartbeats[shardName] = DateTime.UtcNow; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _ = Task.Run(() => PeriodicHeartbeatCleanup(_periodicCheckCts.Token), cancellationToken).ConfigureAwait(false); + return Task.CompletedTask; + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + await _periodicCheckCts.CancelAsync().ConfigureAwait(false); + _periodicCheckCts.Dispose(); + } + + private async Task PeriodicHeartbeatCleanup(CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + foreach (var kvp in _shardHeartbeats.ToFrozenDictionary()) + { + if (DateTime.UtcNow.Subtract(kvp.Value) > TimeSpan.FromMinutes(1)) + { + _shardHeartbeats.TryRemove(kvp.Key, out _); + _shardConfigs.TryRemove(kvp.Key, out _); + } + } + + await Task.Delay(5000, ct).ConfigureAwait(false); + } + } +} diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/RequestQueueService.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/RequestQueueService.cs new file mode 100644 index 0000000..4464c25 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/RequestQueueService.cs @@ -0,0 +1,228 @@ +using LightlessSyncShared.Metrics; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils.Configuration; +using LightlessSyncStaticFilesServer.Utils; +using System.Collections.Concurrent; +using System.Timers; + +namespace LightlessSyncStaticFilesServer.Services; + +public class RequestQueueService : IHostedService +{ + private readonly IClientReadyMessageService _clientReadyMessageService; + private readonly CachedFileProvider _cachedFileProvider; + private readonly ILogger _logger; + private readonly LightlessMetrics _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(LightlessMetrics 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) + { + _logger.LogDebug("Periodic Processing Queue Start"); + _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)); + + if (_queueProcessingSemaphore.CurrentCount == 0) + { + _logger.LogDebug("Aborting Periodic Processing Queue, processing still running"); + 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(); + _logger.LogDebug("Periodic Processing Queue End"); + } + } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/ShardClientReadyMessageService.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/ShardClientReadyMessageService.cs new file mode 100644 index 0000000..8757330 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/ShardClientReadyMessageService.cs @@ -0,0 +1,45 @@ +using LightlessSync.API.Routes; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils; +using LightlessSyncShared.Utils.Configuration; +using System.Net.Http.Headers; + +namespace LightlessSyncStaticFilesServer.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("LightlessSyncServer", "1.0.0.0")); + } + + public async Task SendDownloadReady(string uid, Guid requestId) + { + var mainUrl = _configurationService.GetValue(nameof(StaticFilesServerConfiguration.MainFileServerAddress)); + var path = LightlessFiles.MainSendReadyFullPath(mainUrl, uid, requestId); + using HttpRequestMessage msg = new() + { + RequestUri = path + }; + msg.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _tokenGenerator.Token); + + _logger.LogInformation("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/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/ShardFileCleanupService.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/ShardFileCleanupService.cs new file mode 100644 index 0000000..9615978 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/ShardFileCleanupService.cs @@ -0,0 +1,163 @@ +using ByteSizeLib; +using LightlessSyncShared.Metrics; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils.Configuration; +using Microsoft.EntityFrameworkCore; + +namespace LightlessSyncStaticFilesServer.Services; + +public class ShardFileCleanupService : IHostedService +{ + private readonly string _cacheDir; + private readonly IConfigurationService _configuration; + private readonly ILogger _logger; + private readonly LightlessMetrics _metrics; + private CancellationTokenSource _cleanupCts; + + public ShardFileCleanupService(LightlessMetrics metrics, ILogger logger, IConfigurationService configuration) + { + _metrics = metrics; + _logger = logger; + _configuration = configuration; + _cacheDir = _configuration.GetValue(nameof(StaticFilesServerConfiguration.CacheDirectory)); + } + + public async Task CleanUpTask(CancellationToken ct) + { + _logger.LogInformation("Starting periodic cleanup task"); + + while (!ct.IsCancellationRequested) + { + try + { + DirectoryInfo dir = new(_cacheDir); + var allFiles = dir.GetFiles("*", SearchOption.AllDirectories); + _metrics.SetGaugeTo(MetricsAPI.GaugeFilesTotalSize, allFiles.Sum(f => f.Length)); + _metrics.SetGaugeTo(MetricsAPI.GaugeFilesTotal, allFiles.Length); + + CleanUpOutdatedFiles(ct); + + CleanUpFilesBeyondSizeLimit(ct); + } + catch (Exception e) + { + _logger.LogError(e, "Error during cleanup task"); + } + + var cleanupCheckMinutes = _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.CleanupCheckInMinutes), 15); + + 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); + } + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Cleanup Service started"); + + _cleanupCts = new(); + + _ = CleanUpTask(_cleanupCts.Token); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _cleanupCts.Cancel(); + + return Task.CompletedTask; + } + + private void CleanUpFilesBeyondSizeLimit(CancellationToken ct) + { + var sizeLimit = _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.CacheSizeHardLimitInGiB), -1); + if (sizeLimit <= 0) + { + return; + } + + try + { + _logger.LogInformation("Cleaning up files beyond the cache size limit of {cacheSizeLimit} GiB", sizeLimit); + var allLocalFiles = Directory.EnumerateFiles(_cacheDir, "*", SearchOption.AllDirectories) + .Where(f => !f.EndsWith("dl", StringComparison.OrdinalIgnoreCase)) + .Select(f => new FileInfo(f)).ToList() + .OrderBy(f => f.LastAccessTimeUtc).ToList(); + var totalCacheSizeInBytes = allLocalFiles.Sum(s => s.Length); + long cacheSizeLimitInBytes = (long)ByteSize.FromGibiBytes(sizeLimit).Bytes; + while (totalCacheSizeInBytes > cacheSizeLimitInBytes && allLocalFiles.Any() && !ct.IsCancellationRequested) + { + var oldestFile = allLocalFiles[0]; + allLocalFiles.Remove(oldestFile); + totalCacheSizeInBytes -= oldestFile.Length; + _metrics.DecGauge(MetricsAPI.GaugeFilesTotalSize, oldestFile.Length); + _metrics.DecGauge(MetricsAPI.GaugeFilesTotal); + _logger.LogInformation("Deleting {oldestFile} with size {size}MiB", oldestFile.FullName, ByteSize.FromBytes(oldestFile.Length).MebiBytes); + oldestFile.Delete(); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error during cache size limit cleanup"); + } + } + + private void CleanUpOutdatedFiles(CancellationToken ct) + { + try + { + var unusedRetention = _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UnusedFileRetentionPeriodInDays), 14); + var forcedDeletionAfterHours = _configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.ForcedDeletionOfFilesAfterHours), -1); + + _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 prevTime = DateTime.Now.Subtract(TimeSpan.FromDays(unusedRetention)); + var prevTimeForcedDeletion = DateTime.Now.Subtract(TimeSpan.FromHours(forcedDeletionAfterHours)); + DirectoryInfo dir = new(_cacheDir); + var allFilesInDir = dir.GetFiles("*", SearchOption.AllDirectories) + .Where(f => !f.Name.EndsWith("dl", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + foreach (var file in allFilesInDir) + { + if (file.LastAccessTime < prevTime) + { + _metrics.DecGauge(MetricsAPI.GaugeFilesTotalSize, file.Length); + _metrics.DecGauge(MetricsAPI.GaugeFilesTotal); + _logger.LogInformation("File outdated: {fileName}, {fileSize}MiB", file.Name, ByteSize.FromBytes(file.Length).MebiBytes); + file.Delete(); + } + else if (forcedDeletionAfterHours > 0 && file.LastWriteTime < prevTimeForcedDeletion) + { + _metrics.DecGauge(MetricsAPI.GaugeFilesTotalSize, file.Length); + _metrics.DecGauge(MetricsAPI.GaugeFilesTotal); + _logger.LogInformation("File forcefully deleted: {fileName}, {fileSize}MiB", file.Name, ByteSize.FromBytes(file.Length).MebiBytes); + file.Delete(); + } + else if (file.Length == 0 && !string.Equals(file.Extension, ".dl", StringComparison.OrdinalIgnoreCase)) + { + _metrics.DecGauge(MetricsAPI.GaugeFilesTotalSize, file.Length); + _metrics.DecGauge(MetricsAPI.GaugeFilesTotal); + _logger.LogInformation("File with size 0 deleted: {filename}", file.Name); + file.Delete(); + } + + ct.ThrowIfCancellationRequested(); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error during file cleanup of old files"); + } + } +} diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/ShardRegistrationService.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/ShardRegistrationService.cs new file mode 100644 index 0000000..fef49f9 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/ShardRegistrationService.cs @@ -0,0 +1,107 @@ +using LightlessSync.API.Routes; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils; +using LightlessSyncShared.Utils.Configuration; + +namespace LightlessSyncStaticFilesServer.Services; + +public class ShardRegistrationService : IHostedService +{ + private readonly ILogger _logger; + private readonly IConfigurationService _configurationService; + private readonly HttpClient _httpClient = new(); + private readonly CancellationTokenSource _heartBeatCts = new(); + private bool _isRegistered = false; + + public ShardRegistrationService(ILogger logger, + IConfigurationService configurationService, + ServerTokenGenerator serverTokenGenerator) + { + _logger = logger; + _configurationService = configurationService; + _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", serverTokenGenerator.Token); + } + + private void OnConfigChanged(object sender, EventArgs e) + { + _isRegistered = false; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Starting"); + _configurationService.ConfigChangedEvent += OnConfigChanged; + _ = Task.Run(() => HeartbeatLoop(_heartBeatCts.Token)); + return Task.CompletedTask; + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Stopping"); + + _configurationService.ConfigChangedEvent -= OnConfigChanged; + _heartBeatCts.Cancel(); + _heartBeatCts.Dispose(); + // call unregister + await UnregisterShard().ConfigureAwait(false); + _httpClient.Dispose(); + } + + private async Task HeartbeatLoop(CancellationToken ct) + { + while (!_heartBeatCts.IsCancellationRequested) + { + try + { + await ProcessHeartbeat(ct).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Issue during Heartbeat"); + _isRegistered = false; + } + + await Task.Delay(TimeSpan.FromSeconds(30), ct).ConfigureAwait(false); + } + } + + private async Task ProcessHeartbeat(CancellationToken ct) + { + if (!_isRegistered) + { + await TryRegisterShard(ct).ConfigureAwait(false); + } + + await ShardHeartbeat(ct).ConfigureAwait(false); + } + + private async Task ShardHeartbeat(CancellationToken ct) + { + Uri mainServer = _configurationService.GetValue(nameof(StaticFilesServerConfiguration.MainFileServerAddress)); + _logger.LogInformation("Running heartbeat against Main {server}", mainServer); + + using var heartBeat = await _httpClient.PostAsync(new Uri(mainServer, LightlessFiles.Main + "/shardHeartbeat"), null, ct).ConfigureAwait(false); + heartBeat.EnsureSuccessStatusCode(); + } + + private async Task TryRegisterShard(CancellationToken ct) + { + Uri mainServer = _configurationService.GetValue(nameof(StaticFilesServerConfiguration.MainFileServerAddress)); + _logger.LogInformation("Registering Shard with Main {server}", mainServer); + var config = _configurationService.GetValue(nameof(StaticFilesServerConfiguration.ShardConfiguration)); + _logger.LogInformation("Config Value {varName}: {value}", nameof(ShardConfiguration.Continents), string.Join(", ", config.Continents)); + _logger.LogInformation("Config Value {varName}: {value}", nameof(ShardConfiguration.FileMatch), config.FileMatch); + _logger.LogInformation("Config Value {varName}: {value}", nameof(ShardConfiguration.RegionUris), string.Join("; ", config.RegionUris.Select(k => k.Key + ":" + k.Value))); + + using var register = await _httpClient.PostAsJsonAsync(new Uri(mainServer, LightlessFiles.Main + "/shardRegister"), config, ct).ConfigureAwait(false); + register.EnsureSuccessStatusCode(); + _isRegistered = true; + } + + private async Task UnregisterShard() + { + Uri mainServer = _configurationService.GetValue(nameof(StaticFilesServerConfiguration.MainFileServerAddress)); + _logger.LogInformation("Unregistering Shard with Main {server}", mainServer); + using var heartBeat = await _httpClient.PostAsync(new Uri(mainServer, LightlessFiles.Main + "/shardUnregister"), null).ConfigureAwait(false); + } +} diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Startup.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Startup.cs new file mode 100644 index 0000000..1ed3ec3 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Startup.cs @@ -0,0 +1,272 @@ +using LightlessSyncShared.Data; +using LightlessSyncShared.Metrics; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils; +using LightlessSyncStaticFilesServer.Controllers; +using LightlessSyncStaticFilesServer.Services; +using LightlessSyncStaticFilesServer.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 LightlessSyncShared.Utils.Configuration; + +namespace LightlessSyncStaticFilesServer; + +public class Startup +{ + private bool _isMain; + private bool _isDistributionNode; + private readonly ILogger _logger; + + public Startup(IConfiguration configuration, ILogger logger) + { + Configuration = configuration; + _logger = logger; + var lightlessSettings = Configuration.GetRequiredSection("LightlessSync"); + _isDistributionNode = lightlessSettings.GetValue(nameof(StaticFilesServerConfiguration.IsDistributionNode), false); + _isMain = string.IsNullOrEmpty(lightlessSettings.GetValue(nameof(StaticFilesServerConfiguration.MainFileServerAddress), string.Empty)) && _isDistributionNode; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddHttpContextAccessor(); + + services.AddLogging(); + + services.Configure(Configuration.GetRequiredSection("LightlessSync")); + services.Configure(Configuration.GetRequiredSection("LightlessSync")); + services.Configure(Configuration.GetSection("Kestrel")); + services.AddSingleton(Configuration); + + var lightlessConfig = Configuration.GetRequiredSection("LightlessSync"); + + // metrics configuration + services.AddSingleton(m => new LightlessMetrics(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.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(p => p.GetService()); + services.AddHostedService(m => m.GetService()); + services.AddSingleton, LightlessConfigurationServiceClient>(); + services.AddHostedService(p => (LightlessConfigurationServiceClient)p.GetService>()); + + // specific services + if (_isMain) + { + services.AddSingleton(); + services.AddHostedService(); + services.AddSingleton, LightlessConfigurationServiceServer>(); + services.AddSingleton(); + services.AddHostedService(s => s.GetRequiredService()); + services.AddDbContextPool(options => + { + options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder => + { + builder.MigrationsHistoryTable("_efmigrationshistory", "public"); + }).UseSnakeCaseNamingConvention(); + options.EnableThreadSafetyChecks(false); + }, lightlessConfig.GetValue(nameof(LightlessConfigurationBase.DbContextPoolSize), 1024)); + services.AddDbContextFactory(options => + { + options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder => + { + builder.MigrationsHistoryTable("_efmigrationshistory", "public"); + builder.MigrationsAssembly("LightlessSyncShared"); + }).UseSnakeCaseNamingConvention(); + options.EnableThreadSafetyChecks(false); + }); + + 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 = lightlessConfig.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 = lightlessConfig.GetValue(nameof(ServerConfiguration.RedisPool), 50), + SyncTimeout = options.SyncTimeout, + }; + + services.AddStackExchangeRedisExtensions(redisConfiguration); + } + else + { + services.AddSingleton(); + services.AddHostedService(s => s.GetRequiredService()); + services.AddSingleton(); + services.AddHostedService(); + services.AddSingleton, LightlessConfigurationServiceClient>(); + services.AddHostedService(p => (LightlessConfigurationServiceClient)p.GetService>()); + } + + services.AddMemoryCache(); + + // controller setup + services.AddControllers().ConfigureApplicationPartManager(a => + { + a.FeatureProviders.Remove(a.FeatureProviders.OfType().First()); + if (_isMain) + { + a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(LightlessStaticFilesServerConfigurationController), + typeof(CacheController), typeof(RequestController), typeof(ServerFilesController), + typeof(DistributionController), typeof(MainController), typeof(SpeedTestController))); + } + else if (_isDistributionNode) + { + a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(CacheController), typeof(RequestController), typeof(DistributionController), typeof(SpeedTestController))); + } + else + { + a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(CacheController), typeof(RequestController), typeof(SpeedTestController))); + } + }); + + // authentication and authorization + services.AddOptions(JwtBearerDefaults.AuthenticationScheme) + .Configure>((o, s) => + { + o.TokenValidationParameters = new() + { + ValidateIssuer = false, + ValidateLifetime = true, + ValidateAudience = false, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(s.GetValue(nameof(LightlessConfigurationBase.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(LightlessClaimTypes.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(LightlessConfigurationBase.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/LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/BlockFileDataStream.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/BlockFileDataStream.cs new file mode 100644 index 0000000..7922256 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/BlockFileDataStream.cs @@ -0,0 +1,108 @@ + +namespace LightlessSyncStaticFilesServer.Utils; + +public sealed class BlockFileDataStream : Stream +{ + private readonly IEnumerable _substreams; + private int _currentStreamIndex = 0; + + public BlockFileDataStream(IEnumerable substreams) + { + _substreams = substreams; + } + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => throw new NotSupportedException(); + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + int totalRead = 0; + int currentOffset = 0; + int remainingCount = count; + while (totalRead < count && _currentStreamIndex < _substreams.Count()) + { + var lastReadBytes = await _substreams.ElementAt(_currentStreamIndex).ReadAsync(buffer, currentOffset, remainingCount, cancellationToken).ConfigureAwait(false); + if (lastReadBytes < remainingCount) + { + _substreams.ElementAt(_currentStreamIndex).Dispose(); + _currentStreamIndex++; + } + + totalRead += lastReadBytes; + currentOffset += lastReadBytes; + remainingCount -= lastReadBytes; + } + + return totalRead; + } + + public override int Read(byte[] buffer, int offset, int count) + { + int totalRead = 0; + int currentOffset = 0; + int remainingCount = count; + while (totalRead < count && _currentStreamIndex < _substreams.Count()) + { + var lastReadBytes = _substreams.ElementAt(_currentStreamIndex).Read(buffer, currentOffset, remainingCount); + if (lastReadBytes < remainingCount) + { + _substreams.ElementAt(_currentStreamIndex).Dispose(); + _currentStreamIndex++; + } + + totalRead += lastReadBytes; + currentOffset += lastReadBytes; + remainingCount -= lastReadBytes; + } + + return totalRead; + } + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + int totalRead = 0; + + while (totalRead < buffer.Length && _currentStreamIndex < _substreams.Count()) + { + var substream = _substreams.ElementAt(_currentStreamIndex); + int bytesRead = await substream.ReadAsync(buffer.Slice(totalRead), cancellationToken).ConfigureAwait(false); + + if (bytesRead == 0) + { + substream.Dispose(); + _currentStreamIndex++; + } + else + { + totalRead += bytesRead; + } + } + + return totalRead; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + foreach (var substream in _substreams) + { + // probably unnecessary but better safe than sorry + substream.Dispose(); + } + } + + base.Dispose(disposing); + } + + public override void Flush() => throw new NotSupportedException(); + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); +} diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/BlockFileDataSubstream.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/BlockFileDataSubstream.cs new file mode 100644 index 0000000..02f01c1 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/BlockFileDataSubstream.cs @@ -0,0 +1,115 @@ +using System.Globalization; +using System.Text; + +namespace LightlessSyncStaticFilesServer.Utils; + +public sealed class BlockFileDataSubstream : IDisposable +{ + private readonly MemoryStream _headerStream; + private bool _disposed = false; + private readonly Lazy _dataStreamLazy; + private FileStream DataStream => _dataStreamLazy.Value; + + public BlockFileDataSubstream(FileInfo file) + { + _dataStreamLazy = new(() => File.Open(file.FullName, GetFileStreamOptions(file.Length))); + _headerStream = new MemoryStream(Encoding.ASCII.GetBytes("#" + file.Name + ":" + file.Length.ToString(CultureInfo.InvariantCulture) + "#")); + } + + private static FileStreamOptions GetFileStreamOptions(long fileSize) + { + int bufferSize = fileSize switch + { + <= 128 * 1024 => 0, + <= 512 * 1024 => 4096, + <= 1 * 1024 * 1024 => 65536, + <= 10 * 1024 * 1024 => 131072, + <= 100 * 1024 * 1024 => 524288, + _ => 1048576 + }; + + FileStreamOptions opts = new() + { + Mode = FileMode.Open, + Access = FileAccess.Read, + Share = FileShare.Read | FileShare.Inheritable, + BufferSize = bufferSize + }; + + return opts; + } + + public int Read(byte[] inputBuffer, int offset, int count) + { + int bytesRead = 0; + + // Read from header stream if it has remaining data + if (_headerStream.Position < _headerStream.Length) + { + int headerBytesToRead = (int)Math.Min(count, _headerStream.Length - _headerStream.Position); + bytesRead += _headerStream.Read(inputBuffer, offset, headerBytesToRead); + offset += bytesRead; + count -= bytesRead; + } + + // Read from data stream if there is still space in buffer + if (count > 0 && DataStream.Position < DataStream.Length) + { + bytesRead += DataStream.Read(inputBuffer, offset, count); + } + + return bytesRead; + } + + public async Task ReadAsync(byte[] inputBuffer, int offset, int count, CancellationToken cancellationToken = default) + { + int bytesRead = 0; + + // Async read from header stream + if (_headerStream.Position < _headerStream.Length) + { + int headerBytesToRead = (int)Math.Min(count, _headerStream.Length - _headerStream.Position); + bytesRead += await _headerStream.ReadAsync(inputBuffer.AsMemory(offset, headerBytesToRead), cancellationToken).ConfigureAwait(false); + offset += bytesRead; + count -= bytesRead; + } + + // Async read from data stream + if (count > 0 && DataStream.Position < DataStream.Length) + { + bytesRead += await DataStream.ReadAsync(inputBuffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false); + } + + return bytesRead; + } + + public async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + int bytesRead = 0; + + // Async read from header stream + if (_headerStream.Position < _headerStream.Length) + { + int headerBytesToRead = (int)Math.Min(buffer.Length, _headerStream.Length - _headerStream.Position); + bytesRead += await _headerStream.ReadAsync(buffer.Slice(0, headerBytesToRead), cancellationToken).ConfigureAwait(false); + buffer = buffer.Slice(headerBytesToRead); + } + + // Async read from data stream + if (buffer.Length > 0 && DataStream.Position < DataStream.Length) + { + bytesRead += await DataStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + } + + return bytesRead; + } + + public void Dispose() + { + if (_disposed) return; + _headerStream.Dispose(); + if (_dataStreamLazy.IsValueCreated) + DataStream.Dispose(); + _disposed = true; + } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/FilePathUtil.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/FilePathUtil.cs new file mode 100644 index 0000000..ecc9009 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/FilePathUtil.cs @@ -0,0 +1,31 @@ +namespace LightlessSyncStaticFilesServer.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/LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/RequestFileStreamResult.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/RequestFileStreamResult.cs new file mode 100644 index 0000000..ce76ece --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/RequestFileStreamResult.cs @@ -0,0 +1,58 @@ +using LightlessSyncShared.Metrics; +using LightlessSyncStaticFilesServer.Services; +using Microsoft.AspNetCore.Mvc; + +namespace LightlessSyncStaticFilesServer.Utils; + +public class RequestFileStreamResult : FileStreamResult +{ + private readonly Guid _requestId; + private readonly RequestQueueService _requestQueueService; + private readonly LightlessMetrics _lightlessMetrics; + + public RequestFileStreamResult(Guid requestId, RequestQueueService requestQueueService, LightlessMetrics lightlessMetrics, + Stream fileStream, string contentType) : base(fileStream, contentType) + { + _requestId = requestId; + _requestQueueService = requestQueueService; + _lightlessMetrics = lightlessMetrics; + _lightlessMetrics.IncGauge(MetricsAPI.GaugeCurrentDownloads); + } + + public override void ExecuteResult(ActionContext context) + { + try + { + base.ExecuteResult(context); + } + catch + { + throw; + } + finally + { + _requestQueueService.FinishRequest(_requestId); + + _lightlessMetrics.DecGauge(MetricsAPI.GaugeCurrentDownloads); + FileStream?.Dispose(); + } + } + + public override async Task ExecuteResultAsync(ActionContext context) + { + try + { + await base.ExecuteResultAsync(context).ConfigureAwait(false); + } + catch + { + throw; + } + finally + { + _requestQueueService.FinishRequest(_requestId); + _lightlessMetrics.DecGauge(MetricsAPI.GaugeCurrentDownloads); + FileStream?.Dispose(); + } + } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/RequestFileStreamResultFactory.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/RequestFileStreamResultFactory.cs new file mode 100644 index 0000000..4f720cf --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/RequestFileStreamResultFactory.cs @@ -0,0 +1,26 @@ +using LightlessSyncShared.Metrics; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils.Configuration; +using LightlessSyncStaticFilesServer.Services; + +namespace LightlessSyncStaticFilesServer.Utils; + +public class RequestFileStreamResultFactory +{ + private readonly LightlessMetrics _metrics; + private readonly RequestQueueService _requestQueueService; + private readonly IConfigurationService _configurationService; + + public RequestFileStreamResultFactory(LightlessMetrics metrics, RequestQueueService requestQueueService, IConfigurationService configurationService) + { + _metrics = metrics; + _requestQueueService = requestQueueService; + _configurationService = configurationService; + } + + public RequestFileStreamResult Create(Guid requestId, Stream stream) + { + return new RequestFileStreamResult(requestId, _requestQueueService, + _metrics, stream, "application/octet-stream"); + } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/UserQueueEntry.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/UserQueueEntry.cs new file mode 100644 index 0000000..38d4ea0 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/UserQueueEntry.cs @@ -0,0 +1,21 @@ +namespace LightlessSyncStaticFilesServer.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/LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/UserRequest.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/UserRequest.cs new file mode 100644 index 0000000..28d2e18 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Utils/UserRequest.cs @@ -0,0 +1,6 @@ +namespace LightlessSyncStaticFilesServer.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/LightlessSyncServer/LightlessSyncStaticFilesServer/appsettings.Development.json b/LightlessSyncServer/LightlessSyncStaticFilesServer/appsettings.Development.json new file mode 100644 index 0000000..770d3e9 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/appsettings.json b/LightlessSyncServer/LightlessSyncStaticFilesServer/appsettings.json new file mode 100644 index 0000000..dc53c47 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/appsettings.json @@ -0,0 +1,31 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5432;Database=;Username=;Password=" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://+:5001" + }, + "Gprc": { + "Protocols": "Http2", + "Url": "http://+:5003" + } + } + }, + "LightlessSync": { + "ForcedDeletionOfFilesAfterHours": -1, + "CacheSizeHardLimitInGiB": -1, + "UnusedFileRetentionPeriodInDays": 7, + "CacheDirectory": "G:\\ServerTest", + "ServiceAddress": "http://localhost:5002", + "RemoteCacheSourceUri": "" + }, + "AllowedHosts": "*" +}