diff --git a/backend/Knot.sln b/backend/Knot.sln index 731bc4e..9597f75 100644 --- a/backend/Knot.sln +++ b/backend/Knot.sln @@ -13,19 +13,79 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Shared.Infrastructure" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{EC447DCF-ABFA-6E24-52A5-D7FD48A5C558}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Identity", "Identity", "{0EC62A1A-9858-3A60-8A0C-FC9AACFA2EC7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.Identity", "src\Modules\Identity\Knot.Modules.Identity.csproj", "{5BB2F421-C4C4-40A1-8D6E-FEBB79AE7C98}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Chats", "Chats", "{B4154CE3-9153-E2B0-0515-138FFCE32938}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.Chats", "src\Modules\Chats\Knot.Modules.Chats.csproj", "{695CC919-E321-4DC0-8D70-C631C51624E1}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.Identity.UnitTests", "tests\Knot.Modules.Identity.UnitTests\Knot.Modules.Identity.UnitTests.csproj", "{AB2F334E-DD7C-4314-93B9-941AFC6B2ACD}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Profiles", "Profiles", "{CA5186C2-289A-9324-4152-9805A73A9773}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.Chats.UnitTests", "tests\Knot.Modules.Chats.UnitTests\Knot.Modules.Chats.UnitTests.csproj", "{F48A3E3A-EEBB-43AA-81AB-7DD7FE5F29B4}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.Profiles", "src\Modules\Profiles\Knot.Modules.Profiles.csproj", "{CD14770B-8BCD-4D93-AD2C-581ED030E209}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Relations", "Relations", "{10980DF6-D81A-2FEE-4ACB-4D508FA38BC6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.Relations", "src\Modules\Relations\Knot.Modules.Relations.csproj", "{B97352F3-709A-42CF-A448-07B9996F5FBE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Stories", "Stories", "{F83653D1-5494-68B8-6444-7DEF58635BF6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.Stories", "src\Modules\Stories\Knot.Modules.Stories.csproj", "{BC98E6FC-B46A-4554-B75E-B912714A915C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Storage", "Storage", "{AF61C489-0B09-54BC-BD16-4353135E9C4A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.Storage", "src\Modules\Storage\Knot.Modules.Storage.csproj", "{D052C05A-CDCD-48DF-8F4F-0B10E3719819}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Auth", "Auth", "{E9D55F35-C373-3DD8-6914-3213F2F582A2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.Auth", "src\Modules\Auth\Knot.Modules.Auth.csproj", "{CF08AE9C-B337-47BA-B273-4B8E5610D7BD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.Auth.UnitTests", "tests\Knot.Modules.Auth.UnitTests\Knot.Modules.Auth.UnitTests.csproj", "{BA37DF67-98D3-4399-BD68-9679396C1547}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Messaging", "Messaging", "{AE120A0E-5815-2D46-946C-DA544028AD88}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.Messaging", "src\Modules\Messaging\Knot.Modules.Messaging.csproj", "{C0B5496E-F083-413E-90AE-31282A0ACCA5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TelegramImport", "TelegramImport", "{4D5D7319-366D-1BD8-49C5-A840B487E852}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.TelegramImport", "src\Modules\TelegramImport\Knot.Modules.TelegramImport.csproj", "{C905C120-DE44-4CBC-8F98-5BE148115487}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Conversations", "Conversations", "{138B168B-2473-B06D-4BD5-1B010DA319A1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.Conversations", "src\Modules\Conversations\Knot.Modules.Conversations.csproj", "{F3EFD119-44F7-46BF-A03C-060F30A1A1E0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.Admin.UnitTests", "Tests\Knot.Modules.Admin.UnitTests\Knot.Modules.Admin.UnitTests.csproj", "{489D5878-3945-4F8E-AD13-E5499A18DFCC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Admin", "Admin", "{E9E1155A-A339-766C-C185-6698FF423EBC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.Admin", "src\Modules\Admin\Knot.Modules.Admin.csproj", "{FFC12440-F128-48F5-9BE5-E5263930F876}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.Federation.UnitTests", "Tests\Knot.Modules.Federation.UnitTests\Knot.Modules.Federation.UnitTests.csproj", "{7A402C99-2E66-4EBE-A257-0D7923A96DFD}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Federation", "Federation", "{837D2CF4-7AEA-BF76-6362-47ECD7058EEC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.Federation", "src\Modules\Federation\Knot.Modules.Federation.csproj", "{716E089D-0661-4D39-813B-063DF9A4C0E4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.Klipy.UnitTests", "Tests\Knot.Modules.Klipy.UnitTests\Knot.Modules.Klipy.UnitTests.csproj", "{43E6E19C-39EC-49C8-91EE-49586A4B9D03}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Klipy", "Klipy", "{4BC86D3F-8D7B-ACB9-EA51-9374BC9D421A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.Klipy", "src\Modules\Klipy\Knot.Modules.Klipy.csproj", "{2498C58C-53B0-4DF2-B4EA-BA69B35A7488}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.Stories.UnitTests", "Tests\Knot.Modules.Stories.UnitTests\Knot.Modules.Stories.UnitTests.csproj", "{4394178B-1ADD-478E-BF73-574B31C11292}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.Relations.UnitTests", "Tests\Knot.Modules.Relations.UnitTests\Knot.Modules.Relations.UnitTests.csproj", "{2F2C7E01-5791-4A6D-9A80-C71CAD846FA8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.Settings.UnitTests", "Tests\Knot.Modules.Settings.UnitTests\Knot.Modules.Settings.UnitTests.csproj", "{76706BA9-11E0-463A-AF8E-18DFBD9E95A2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Settings", "Settings", "{0587C5BB-3015-204D-19B6-EB01E03E48B6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.Settings", "src\Modules\Settings\Knot.Modules.Settings.csproj", "{AEFD93B4-59B8-4B02-A694-CA15E4EC1C08}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.Storage.UnitTests", "Tests\Knot.Modules.Storage.UnitTests\Knot.Modules.Storage.UnitTests.csproj", "{C11BFE93-017D-4538-A61F-A8D98F8D2D75}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.TelegramImport.UnitTests", "Tests\Knot.Modules.TelegramImport.UnitTests\Knot.Modules.TelegramImport.UnitTests.csproj", "{38276FFF-05D8-4005-8863-018CEE833D66}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.WebRtc.UnitTests", "Tests\Knot.Modules.WebRtc.UnitTests\Knot.Modules.WebRtc.UnitTests.csproj", "{32AC7906-B197-4628-9C47-9337BB5A53E2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebRtc", "WebRtc", "{DD6FAF9E-10A0-95E7-0C06-698A6F70AFEE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knot.Modules.WebRtc", "src\Modules\WebRtc\Knot.Modules.WebRtc.csproj", "{03BE4EB1-FD38-445A-9A4B-7B481560E9A2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -61,54 +121,282 @@ Global {6C13E9A4-234C-4BF1-A145-F1BBA2AC56CB}.Release|x64.Build.0 = Release|Any CPU {6C13E9A4-234C-4BF1-A145-F1BBA2AC56CB}.Release|x86.ActiveCfg = Release|Any CPU {6C13E9A4-234C-4BF1-A145-F1BBA2AC56CB}.Release|x86.Build.0 = Release|Any CPU - {5BB2F421-C4C4-40A1-8D6E-FEBB79AE7C98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5BB2F421-C4C4-40A1-8D6E-FEBB79AE7C98}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5BB2F421-C4C4-40A1-8D6E-FEBB79AE7C98}.Debug|x64.ActiveCfg = Debug|Any CPU - {5BB2F421-C4C4-40A1-8D6E-FEBB79AE7C98}.Debug|x64.Build.0 = Debug|Any CPU - {5BB2F421-C4C4-40A1-8D6E-FEBB79AE7C98}.Debug|x86.ActiveCfg = Debug|Any CPU - {5BB2F421-C4C4-40A1-8D6E-FEBB79AE7C98}.Debug|x86.Build.0 = Debug|Any CPU - {5BB2F421-C4C4-40A1-8D6E-FEBB79AE7C98}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5BB2F421-C4C4-40A1-8D6E-FEBB79AE7C98}.Release|Any CPU.Build.0 = Release|Any CPU - {5BB2F421-C4C4-40A1-8D6E-FEBB79AE7C98}.Release|x64.ActiveCfg = Release|Any CPU - {5BB2F421-C4C4-40A1-8D6E-FEBB79AE7C98}.Release|x64.Build.0 = Release|Any CPU - {5BB2F421-C4C4-40A1-8D6E-FEBB79AE7C98}.Release|x86.ActiveCfg = Release|Any CPU - {5BB2F421-C4C4-40A1-8D6E-FEBB79AE7C98}.Release|x86.Build.0 = Release|Any CPU - {695CC919-E321-4DC0-8D70-C631C51624E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {695CC919-E321-4DC0-8D70-C631C51624E1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {695CC919-E321-4DC0-8D70-C631C51624E1}.Debug|x64.ActiveCfg = Debug|Any CPU - {695CC919-E321-4DC0-8D70-C631C51624E1}.Debug|x64.Build.0 = Debug|Any CPU - {695CC919-E321-4DC0-8D70-C631C51624E1}.Debug|x86.ActiveCfg = Debug|Any CPU - {695CC919-E321-4DC0-8D70-C631C51624E1}.Debug|x86.Build.0 = Debug|Any CPU - {695CC919-E321-4DC0-8D70-C631C51624E1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {695CC919-E321-4DC0-8D70-C631C51624E1}.Release|Any CPU.Build.0 = Release|Any CPU - {695CC919-E321-4DC0-8D70-C631C51624E1}.Release|x64.ActiveCfg = Release|Any CPU - {695CC919-E321-4DC0-8D70-C631C51624E1}.Release|x64.Build.0 = Release|Any CPU - {695CC919-E321-4DC0-8D70-C631C51624E1}.Release|x86.ActiveCfg = Release|Any CPU - {695CC919-E321-4DC0-8D70-C631C51624E1}.Release|x86.Build.0 = Release|Any CPU - {AB2F334E-DD7C-4314-93B9-941AFC6B2ACD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AB2F334E-DD7C-4314-93B9-941AFC6B2ACD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AB2F334E-DD7C-4314-93B9-941AFC6B2ACD}.Debug|x64.ActiveCfg = Debug|Any CPU - {AB2F334E-DD7C-4314-93B9-941AFC6B2ACD}.Debug|x64.Build.0 = Debug|Any CPU - {AB2F334E-DD7C-4314-93B9-941AFC6B2ACD}.Debug|x86.ActiveCfg = Debug|Any CPU - {AB2F334E-DD7C-4314-93B9-941AFC6B2ACD}.Debug|x86.Build.0 = Debug|Any CPU - {AB2F334E-DD7C-4314-93B9-941AFC6B2ACD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AB2F334E-DD7C-4314-93B9-941AFC6B2ACD}.Release|Any CPU.Build.0 = Release|Any CPU - {AB2F334E-DD7C-4314-93B9-941AFC6B2ACD}.Release|x64.ActiveCfg = Release|Any CPU - {AB2F334E-DD7C-4314-93B9-941AFC6B2ACD}.Release|x64.Build.0 = Release|Any CPU - {AB2F334E-DD7C-4314-93B9-941AFC6B2ACD}.Release|x86.ActiveCfg = Release|Any CPU - {AB2F334E-DD7C-4314-93B9-941AFC6B2ACD}.Release|x86.Build.0 = Release|Any CPU - {F48A3E3A-EEBB-43AA-81AB-7DD7FE5F29B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F48A3E3A-EEBB-43AA-81AB-7DD7FE5F29B4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F48A3E3A-EEBB-43AA-81AB-7DD7FE5F29B4}.Debug|x64.ActiveCfg = Debug|Any CPU - {F48A3E3A-EEBB-43AA-81AB-7DD7FE5F29B4}.Debug|x64.Build.0 = Debug|Any CPU - {F48A3E3A-EEBB-43AA-81AB-7DD7FE5F29B4}.Debug|x86.ActiveCfg = Debug|Any CPU - {F48A3E3A-EEBB-43AA-81AB-7DD7FE5F29B4}.Debug|x86.Build.0 = Debug|Any CPU - {F48A3E3A-EEBB-43AA-81AB-7DD7FE5F29B4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F48A3E3A-EEBB-43AA-81AB-7DD7FE5F29B4}.Release|Any CPU.Build.0 = Release|Any CPU - {F48A3E3A-EEBB-43AA-81AB-7DD7FE5F29B4}.Release|x64.ActiveCfg = Release|Any CPU - {F48A3E3A-EEBB-43AA-81AB-7DD7FE5F29B4}.Release|x64.Build.0 = Release|Any CPU - {F48A3E3A-EEBB-43AA-81AB-7DD7FE5F29B4}.Release|x86.ActiveCfg = Release|Any CPU - {F48A3E3A-EEBB-43AA-81AB-7DD7FE5F29B4}.Release|x86.Build.0 = Release|Any CPU + {CD14770B-8BCD-4D93-AD2C-581ED030E209}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD14770B-8BCD-4D93-AD2C-581ED030E209}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD14770B-8BCD-4D93-AD2C-581ED030E209}.Debug|x64.ActiveCfg = Debug|Any CPU + {CD14770B-8BCD-4D93-AD2C-581ED030E209}.Debug|x64.Build.0 = Debug|Any CPU + {CD14770B-8BCD-4D93-AD2C-581ED030E209}.Debug|x86.ActiveCfg = Debug|Any CPU + {CD14770B-8BCD-4D93-AD2C-581ED030E209}.Debug|x86.Build.0 = Debug|Any CPU + {CD14770B-8BCD-4D93-AD2C-581ED030E209}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD14770B-8BCD-4D93-AD2C-581ED030E209}.Release|Any CPU.Build.0 = Release|Any CPU + {CD14770B-8BCD-4D93-AD2C-581ED030E209}.Release|x64.ActiveCfg = Release|Any CPU + {CD14770B-8BCD-4D93-AD2C-581ED030E209}.Release|x64.Build.0 = Release|Any CPU + {CD14770B-8BCD-4D93-AD2C-581ED030E209}.Release|x86.ActiveCfg = Release|Any CPU + {CD14770B-8BCD-4D93-AD2C-581ED030E209}.Release|x86.Build.0 = Release|Any CPU + {B97352F3-709A-42CF-A448-07B9996F5FBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B97352F3-709A-42CF-A448-07B9996F5FBE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B97352F3-709A-42CF-A448-07B9996F5FBE}.Debug|x64.ActiveCfg = Debug|Any CPU + {B97352F3-709A-42CF-A448-07B9996F5FBE}.Debug|x64.Build.0 = Debug|Any CPU + {B97352F3-709A-42CF-A448-07B9996F5FBE}.Debug|x86.ActiveCfg = Debug|Any CPU + {B97352F3-709A-42CF-A448-07B9996F5FBE}.Debug|x86.Build.0 = Debug|Any CPU + {B97352F3-709A-42CF-A448-07B9996F5FBE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B97352F3-709A-42CF-A448-07B9996F5FBE}.Release|Any CPU.Build.0 = Release|Any CPU + {B97352F3-709A-42CF-A448-07B9996F5FBE}.Release|x64.ActiveCfg = Release|Any CPU + {B97352F3-709A-42CF-A448-07B9996F5FBE}.Release|x64.Build.0 = Release|Any CPU + {B97352F3-709A-42CF-A448-07B9996F5FBE}.Release|x86.ActiveCfg = Release|Any CPU + {B97352F3-709A-42CF-A448-07B9996F5FBE}.Release|x86.Build.0 = Release|Any CPU + {BC98E6FC-B46A-4554-B75E-B912714A915C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC98E6FC-B46A-4554-B75E-B912714A915C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC98E6FC-B46A-4554-B75E-B912714A915C}.Debug|x64.ActiveCfg = Debug|Any CPU + {BC98E6FC-B46A-4554-B75E-B912714A915C}.Debug|x64.Build.0 = Debug|Any CPU + {BC98E6FC-B46A-4554-B75E-B912714A915C}.Debug|x86.ActiveCfg = Debug|Any CPU + {BC98E6FC-B46A-4554-B75E-B912714A915C}.Debug|x86.Build.0 = Debug|Any CPU + {BC98E6FC-B46A-4554-B75E-B912714A915C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC98E6FC-B46A-4554-B75E-B912714A915C}.Release|Any CPU.Build.0 = Release|Any CPU + {BC98E6FC-B46A-4554-B75E-B912714A915C}.Release|x64.ActiveCfg = Release|Any CPU + {BC98E6FC-B46A-4554-B75E-B912714A915C}.Release|x64.Build.0 = Release|Any CPU + {BC98E6FC-B46A-4554-B75E-B912714A915C}.Release|x86.ActiveCfg = Release|Any CPU + {BC98E6FC-B46A-4554-B75E-B912714A915C}.Release|x86.Build.0 = Release|Any CPU + {D052C05A-CDCD-48DF-8F4F-0B10E3719819}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D052C05A-CDCD-48DF-8F4F-0B10E3719819}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D052C05A-CDCD-48DF-8F4F-0B10E3719819}.Debug|x64.ActiveCfg = Debug|Any CPU + {D052C05A-CDCD-48DF-8F4F-0B10E3719819}.Debug|x64.Build.0 = Debug|Any CPU + {D052C05A-CDCD-48DF-8F4F-0B10E3719819}.Debug|x86.ActiveCfg = Debug|Any CPU + {D052C05A-CDCD-48DF-8F4F-0B10E3719819}.Debug|x86.Build.0 = Debug|Any CPU + {D052C05A-CDCD-48DF-8F4F-0B10E3719819}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D052C05A-CDCD-48DF-8F4F-0B10E3719819}.Release|Any CPU.Build.0 = Release|Any CPU + {D052C05A-CDCD-48DF-8F4F-0B10E3719819}.Release|x64.ActiveCfg = Release|Any CPU + {D052C05A-CDCD-48DF-8F4F-0B10E3719819}.Release|x64.Build.0 = Release|Any CPU + {D052C05A-CDCD-48DF-8F4F-0B10E3719819}.Release|x86.ActiveCfg = Release|Any CPU + {D052C05A-CDCD-48DF-8F4F-0B10E3719819}.Release|x86.Build.0 = Release|Any CPU + {CF08AE9C-B337-47BA-B273-4B8E5610D7BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF08AE9C-B337-47BA-B273-4B8E5610D7BD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF08AE9C-B337-47BA-B273-4B8E5610D7BD}.Debug|x64.ActiveCfg = Debug|Any CPU + {CF08AE9C-B337-47BA-B273-4B8E5610D7BD}.Debug|x64.Build.0 = Debug|Any CPU + {CF08AE9C-B337-47BA-B273-4B8E5610D7BD}.Debug|x86.ActiveCfg = Debug|Any CPU + {CF08AE9C-B337-47BA-B273-4B8E5610D7BD}.Debug|x86.Build.0 = Debug|Any CPU + {CF08AE9C-B337-47BA-B273-4B8E5610D7BD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF08AE9C-B337-47BA-B273-4B8E5610D7BD}.Release|Any CPU.Build.0 = Release|Any CPU + {CF08AE9C-B337-47BA-B273-4B8E5610D7BD}.Release|x64.ActiveCfg = Release|Any CPU + {CF08AE9C-B337-47BA-B273-4B8E5610D7BD}.Release|x64.Build.0 = Release|Any CPU + {CF08AE9C-B337-47BA-B273-4B8E5610D7BD}.Release|x86.ActiveCfg = Release|Any CPU + {CF08AE9C-B337-47BA-B273-4B8E5610D7BD}.Release|x86.Build.0 = Release|Any CPU + {BA37DF67-98D3-4399-BD68-9679396C1547}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BA37DF67-98D3-4399-BD68-9679396C1547}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA37DF67-98D3-4399-BD68-9679396C1547}.Debug|x64.ActiveCfg = Debug|Any CPU + {BA37DF67-98D3-4399-BD68-9679396C1547}.Debug|x64.Build.0 = Debug|Any CPU + {BA37DF67-98D3-4399-BD68-9679396C1547}.Debug|x86.ActiveCfg = Debug|Any CPU + {BA37DF67-98D3-4399-BD68-9679396C1547}.Debug|x86.Build.0 = Debug|Any CPU + {BA37DF67-98D3-4399-BD68-9679396C1547}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BA37DF67-98D3-4399-BD68-9679396C1547}.Release|Any CPU.Build.0 = Release|Any CPU + {BA37DF67-98D3-4399-BD68-9679396C1547}.Release|x64.ActiveCfg = Release|Any CPU + {BA37DF67-98D3-4399-BD68-9679396C1547}.Release|x64.Build.0 = Release|Any CPU + {BA37DF67-98D3-4399-BD68-9679396C1547}.Release|x86.ActiveCfg = Release|Any CPU + {BA37DF67-98D3-4399-BD68-9679396C1547}.Release|x86.Build.0 = Release|Any CPU + {C0B5496E-F083-413E-90AE-31282A0ACCA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0B5496E-F083-413E-90AE-31282A0ACCA5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0B5496E-F083-413E-90AE-31282A0ACCA5}.Debug|x64.ActiveCfg = Debug|Any CPU + {C0B5496E-F083-413E-90AE-31282A0ACCA5}.Debug|x64.Build.0 = Debug|Any CPU + {C0B5496E-F083-413E-90AE-31282A0ACCA5}.Debug|x86.ActiveCfg = Debug|Any CPU + {C0B5496E-F083-413E-90AE-31282A0ACCA5}.Debug|x86.Build.0 = Debug|Any CPU + {C0B5496E-F083-413E-90AE-31282A0ACCA5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0B5496E-F083-413E-90AE-31282A0ACCA5}.Release|Any CPU.Build.0 = Release|Any CPU + {C0B5496E-F083-413E-90AE-31282A0ACCA5}.Release|x64.ActiveCfg = Release|Any CPU + {C0B5496E-F083-413E-90AE-31282A0ACCA5}.Release|x64.Build.0 = Release|Any CPU + {C0B5496E-F083-413E-90AE-31282A0ACCA5}.Release|x86.ActiveCfg = Release|Any CPU + {C0B5496E-F083-413E-90AE-31282A0ACCA5}.Release|x86.Build.0 = Release|Any CPU + {C905C120-DE44-4CBC-8F98-5BE148115487}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C905C120-DE44-4CBC-8F98-5BE148115487}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C905C120-DE44-4CBC-8F98-5BE148115487}.Debug|x64.ActiveCfg = Debug|Any CPU + {C905C120-DE44-4CBC-8F98-5BE148115487}.Debug|x64.Build.0 = Debug|Any CPU + {C905C120-DE44-4CBC-8F98-5BE148115487}.Debug|x86.ActiveCfg = Debug|Any CPU + {C905C120-DE44-4CBC-8F98-5BE148115487}.Debug|x86.Build.0 = Debug|Any CPU + {C905C120-DE44-4CBC-8F98-5BE148115487}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C905C120-DE44-4CBC-8F98-5BE148115487}.Release|Any CPU.Build.0 = Release|Any CPU + {C905C120-DE44-4CBC-8F98-5BE148115487}.Release|x64.ActiveCfg = Release|Any CPU + {C905C120-DE44-4CBC-8F98-5BE148115487}.Release|x64.Build.0 = Release|Any CPU + {C905C120-DE44-4CBC-8F98-5BE148115487}.Release|x86.ActiveCfg = Release|Any CPU + {C905C120-DE44-4CBC-8F98-5BE148115487}.Release|x86.Build.0 = Release|Any CPU + {F3EFD119-44F7-46BF-A03C-060F30A1A1E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3EFD119-44F7-46BF-A03C-060F30A1A1E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3EFD119-44F7-46BF-A03C-060F30A1A1E0}.Debug|x64.ActiveCfg = Debug|Any CPU + {F3EFD119-44F7-46BF-A03C-060F30A1A1E0}.Debug|x64.Build.0 = Debug|Any CPU + {F3EFD119-44F7-46BF-A03C-060F30A1A1E0}.Debug|x86.ActiveCfg = Debug|Any CPU + {F3EFD119-44F7-46BF-A03C-060F30A1A1E0}.Debug|x86.Build.0 = Debug|Any CPU + {F3EFD119-44F7-46BF-A03C-060F30A1A1E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3EFD119-44F7-46BF-A03C-060F30A1A1E0}.Release|Any CPU.Build.0 = Release|Any CPU + {F3EFD119-44F7-46BF-A03C-060F30A1A1E0}.Release|x64.ActiveCfg = Release|Any CPU + {F3EFD119-44F7-46BF-A03C-060F30A1A1E0}.Release|x64.Build.0 = Release|Any CPU + {F3EFD119-44F7-46BF-A03C-060F30A1A1E0}.Release|x86.ActiveCfg = Release|Any CPU + {F3EFD119-44F7-46BF-A03C-060F30A1A1E0}.Release|x86.Build.0 = Release|Any CPU + {489D5878-3945-4F8E-AD13-E5499A18DFCC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {489D5878-3945-4F8E-AD13-E5499A18DFCC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {489D5878-3945-4F8E-AD13-E5499A18DFCC}.Debug|x64.ActiveCfg = Debug|Any CPU + {489D5878-3945-4F8E-AD13-E5499A18DFCC}.Debug|x64.Build.0 = Debug|Any CPU + {489D5878-3945-4F8E-AD13-E5499A18DFCC}.Debug|x86.ActiveCfg = Debug|Any CPU + {489D5878-3945-4F8E-AD13-E5499A18DFCC}.Debug|x86.Build.0 = Debug|Any CPU + {489D5878-3945-4F8E-AD13-E5499A18DFCC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {489D5878-3945-4F8E-AD13-E5499A18DFCC}.Release|Any CPU.Build.0 = Release|Any CPU + {489D5878-3945-4F8E-AD13-E5499A18DFCC}.Release|x64.ActiveCfg = Release|Any CPU + {489D5878-3945-4F8E-AD13-E5499A18DFCC}.Release|x64.Build.0 = Release|Any CPU + {489D5878-3945-4F8E-AD13-E5499A18DFCC}.Release|x86.ActiveCfg = Release|Any CPU + {489D5878-3945-4F8E-AD13-E5499A18DFCC}.Release|x86.Build.0 = Release|Any CPU + {FFC12440-F128-48F5-9BE5-E5263930F876}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FFC12440-F128-48F5-9BE5-E5263930F876}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FFC12440-F128-48F5-9BE5-E5263930F876}.Debug|x64.ActiveCfg = Debug|Any CPU + {FFC12440-F128-48F5-9BE5-E5263930F876}.Debug|x64.Build.0 = Debug|Any CPU + {FFC12440-F128-48F5-9BE5-E5263930F876}.Debug|x86.ActiveCfg = Debug|Any CPU + {FFC12440-F128-48F5-9BE5-E5263930F876}.Debug|x86.Build.0 = Debug|Any CPU + {FFC12440-F128-48F5-9BE5-E5263930F876}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FFC12440-F128-48F5-9BE5-E5263930F876}.Release|Any CPU.Build.0 = Release|Any CPU + {FFC12440-F128-48F5-9BE5-E5263930F876}.Release|x64.ActiveCfg = Release|Any CPU + {FFC12440-F128-48F5-9BE5-E5263930F876}.Release|x64.Build.0 = Release|Any CPU + {FFC12440-F128-48F5-9BE5-E5263930F876}.Release|x86.ActiveCfg = Release|Any CPU + {FFC12440-F128-48F5-9BE5-E5263930F876}.Release|x86.Build.0 = Release|Any CPU + {7A402C99-2E66-4EBE-A257-0D7923A96DFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7A402C99-2E66-4EBE-A257-0D7923A96DFD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7A402C99-2E66-4EBE-A257-0D7923A96DFD}.Debug|x64.ActiveCfg = Debug|Any CPU + {7A402C99-2E66-4EBE-A257-0D7923A96DFD}.Debug|x64.Build.0 = Debug|Any CPU + {7A402C99-2E66-4EBE-A257-0D7923A96DFD}.Debug|x86.ActiveCfg = Debug|Any CPU + {7A402C99-2E66-4EBE-A257-0D7923A96DFD}.Debug|x86.Build.0 = Debug|Any CPU + {7A402C99-2E66-4EBE-A257-0D7923A96DFD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7A402C99-2E66-4EBE-A257-0D7923A96DFD}.Release|Any CPU.Build.0 = Release|Any CPU + {7A402C99-2E66-4EBE-A257-0D7923A96DFD}.Release|x64.ActiveCfg = Release|Any CPU + {7A402C99-2E66-4EBE-A257-0D7923A96DFD}.Release|x64.Build.0 = Release|Any CPU + {7A402C99-2E66-4EBE-A257-0D7923A96DFD}.Release|x86.ActiveCfg = Release|Any CPU + {7A402C99-2E66-4EBE-A257-0D7923A96DFD}.Release|x86.Build.0 = Release|Any CPU + {716E089D-0661-4D39-813B-063DF9A4C0E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {716E089D-0661-4D39-813B-063DF9A4C0E4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {716E089D-0661-4D39-813B-063DF9A4C0E4}.Debug|x64.ActiveCfg = Debug|Any CPU + {716E089D-0661-4D39-813B-063DF9A4C0E4}.Debug|x64.Build.0 = Debug|Any CPU + {716E089D-0661-4D39-813B-063DF9A4C0E4}.Debug|x86.ActiveCfg = Debug|Any CPU + {716E089D-0661-4D39-813B-063DF9A4C0E4}.Debug|x86.Build.0 = Debug|Any CPU + {716E089D-0661-4D39-813B-063DF9A4C0E4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {716E089D-0661-4D39-813B-063DF9A4C0E4}.Release|Any CPU.Build.0 = Release|Any CPU + {716E089D-0661-4D39-813B-063DF9A4C0E4}.Release|x64.ActiveCfg = Release|Any CPU + {716E089D-0661-4D39-813B-063DF9A4C0E4}.Release|x64.Build.0 = Release|Any CPU + {716E089D-0661-4D39-813B-063DF9A4C0E4}.Release|x86.ActiveCfg = Release|Any CPU + {716E089D-0661-4D39-813B-063DF9A4C0E4}.Release|x86.Build.0 = Release|Any CPU + {43E6E19C-39EC-49C8-91EE-49586A4B9D03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {43E6E19C-39EC-49C8-91EE-49586A4B9D03}.Debug|Any CPU.Build.0 = Debug|Any CPU + {43E6E19C-39EC-49C8-91EE-49586A4B9D03}.Debug|x64.ActiveCfg = Debug|Any CPU + {43E6E19C-39EC-49C8-91EE-49586A4B9D03}.Debug|x64.Build.0 = Debug|Any CPU + {43E6E19C-39EC-49C8-91EE-49586A4B9D03}.Debug|x86.ActiveCfg = Debug|Any CPU + {43E6E19C-39EC-49C8-91EE-49586A4B9D03}.Debug|x86.Build.0 = Debug|Any CPU + {43E6E19C-39EC-49C8-91EE-49586A4B9D03}.Release|Any CPU.ActiveCfg = Release|Any CPU + {43E6E19C-39EC-49C8-91EE-49586A4B9D03}.Release|Any CPU.Build.0 = Release|Any CPU + {43E6E19C-39EC-49C8-91EE-49586A4B9D03}.Release|x64.ActiveCfg = Release|Any CPU + {43E6E19C-39EC-49C8-91EE-49586A4B9D03}.Release|x64.Build.0 = Release|Any CPU + {43E6E19C-39EC-49C8-91EE-49586A4B9D03}.Release|x86.ActiveCfg = Release|Any CPU + {43E6E19C-39EC-49C8-91EE-49586A4B9D03}.Release|x86.Build.0 = Release|Any CPU + {2498C58C-53B0-4DF2-B4EA-BA69B35A7488}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2498C58C-53B0-4DF2-B4EA-BA69B35A7488}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2498C58C-53B0-4DF2-B4EA-BA69B35A7488}.Debug|x64.ActiveCfg = Debug|Any CPU + {2498C58C-53B0-4DF2-B4EA-BA69B35A7488}.Debug|x64.Build.0 = Debug|Any CPU + {2498C58C-53B0-4DF2-B4EA-BA69B35A7488}.Debug|x86.ActiveCfg = Debug|Any CPU + {2498C58C-53B0-4DF2-B4EA-BA69B35A7488}.Debug|x86.Build.0 = Debug|Any CPU + {2498C58C-53B0-4DF2-B4EA-BA69B35A7488}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2498C58C-53B0-4DF2-B4EA-BA69B35A7488}.Release|Any CPU.Build.0 = Release|Any CPU + {2498C58C-53B0-4DF2-B4EA-BA69B35A7488}.Release|x64.ActiveCfg = Release|Any CPU + {2498C58C-53B0-4DF2-B4EA-BA69B35A7488}.Release|x64.Build.0 = Release|Any CPU + {2498C58C-53B0-4DF2-B4EA-BA69B35A7488}.Release|x86.ActiveCfg = Release|Any CPU + {2498C58C-53B0-4DF2-B4EA-BA69B35A7488}.Release|x86.Build.0 = Release|Any CPU + {4394178B-1ADD-478E-BF73-574B31C11292}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4394178B-1ADD-478E-BF73-574B31C11292}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4394178B-1ADD-478E-BF73-574B31C11292}.Debug|x64.ActiveCfg = Debug|Any CPU + {4394178B-1ADD-478E-BF73-574B31C11292}.Debug|x64.Build.0 = Debug|Any CPU + {4394178B-1ADD-478E-BF73-574B31C11292}.Debug|x86.ActiveCfg = Debug|Any CPU + {4394178B-1ADD-478E-BF73-574B31C11292}.Debug|x86.Build.0 = Debug|Any CPU + {4394178B-1ADD-478E-BF73-574B31C11292}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4394178B-1ADD-478E-BF73-574B31C11292}.Release|Any CPU.Build.0 = Release|Any CPU + {4394178B-1ADD-478E-BF73-574B31C11292}.Release|x64.ActiveCfg = Release|Any CPU + {4394178B-1ADD-478E-BF73-574B31C11292}.Release|x64.Build.0 = Release|Any CPU + {4394178B-1ADD-478E-BF73-574B31C11292}.Release|x86.ActiveCfg = Release|Any CPU + {4394178B-1ADD-478E-BF73-574B31C11292}.Release|x86.Build.0 = Release|Any CPU + {2F2C7E01-5791-4A6D-9A80-C71CAD846FA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F2C7E01-5791-4A6D-9A80-C71CAD846FA8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F2C7E01-5791-4A6D-9A80-C71CAD846FA8}.Debug|x64.ActiveCfg = Debug|Any CPU + {2F2C7E01-5791-4A6D-9A80-C71CAD846FA8}.Debug|x64.Build.0 = Debug|Any CPU + {2F2C7E01-5791-4A6D-9A80-C71CAD846FA8}.Debug|x86.ActiveCfg = Debug|Any CPU + {2F2C7E01-5791-4A6D-9A80-C71CAD846FA8}.Debug|x86.Build.0 = Debug|Any CPU + {2F2C7E01-5791-4A6D-9A80-C71CAD846FA8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F2C7E01-5791-4A6D-9A80-C71CAD846FA8}.Release|Any CPU.Build.0 = Release|Any CPU + {2F2C7E01-5791-4A6D-9A80-C71CAD846FA8}.Release|x64.ActiveCfg = Release|Any CPU + {2F2C7E01-5791-4A6D-9A80-C71CAD846FA8}.Release|x64.Build.0 = Release|Any CPU + {2F2C7E01-5791-4A6D-9A80-C71CAD846FA8}.Release|x86.ActiveCfg = Release|Any CPU + {2F2C7E01-5791-4A6D-9A80-C71CAD846FA8}.Release|x86.Build.0 = Release|Any CPU + {76706BA9-11E0-463A-AF8E-18DFBD9E95A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {76706BA9-11E0-463A-AF8E-18DFBD9E95A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {76706BA9-11E0-463A-AF8E-18DFBD9E95A2}.Debug|x64.ActiveCfg = Debug|Any CPU + {76706BA9-11E0-463A-AF8E-18DFBD9E95A2}.Debug|x64.Build.0 = Debug|Any CPU + {76706BA9-11E0-463A-AF8E-18DFBD9E95A2}.Debug|x86.ActiveCfg = Debug|Any CPU + {76706BA9-11E0-463A-AF8E-18DFBD9E95A2}.Debug|x86.Build.0 = Debug|Any CPU + {76706BA9-11E0-463A-AF8E-18DFBD9E95A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {76706BA9-11E0-463A-AF8E-18DFBD9E95A2}.Release|Any CPU.Build.0 = Release|Any CPU + {76706BA9-11E0-463A-AF8E-18DFBD9E95A2}.Release|x64.ActiveCfg = Release|Any CPU + {76706BA9-11E0-463A-AF8E-18DFBD9E95A2}.Release|x64.Build.0 = Release|Any CPU + {76706BA9-11E0-463A-AF8E-18DFBD9E95A2}.Release|x86.ActiveCfg = Release|Any CPU + {76706BA9-11E0-463A-AF8E-18DFBD9E95A2}.Release|x86.Build.0 = Release|Any CPU + {AEFD93B4-59B8-4B02-A694-CA15E4EC1C08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AEFD93B4-59B8-4B02-A694-CA15E4EC1C08}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AEFD93B4-59B8-4B02-A694-CA15E4EC1C08}.Debug|x64.ActiveCfg = Debug|Any CPU + {AEFD93B4-59B8-4B02-A694-CA15E4EC1C08}.Debug|x64.Build.0 = Debug|Any CPU + {AEFD93B4-59B8-4B02-A694-CA15E4EC1C08}.Debug|x86.ActiveCfg = Debug|Any CPU + {AEFD93B4-59B8-4B02-A694-CA15E4EC1C08}.Debug|x86.Build.0 = Debug|Any CPU + {AEFD93B4-59B8-4B02-A694-CA15E4EC1C08}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AEFD93B4-59B8-4B02-A694-CA15E4EC1C08}.Release|Any CPU.Build.0 = Release|Any CPU + {AEFD93B4-59B8-4B02-A694-CA15E4EC1C08}.Release|x64.ActiveCfg = Release|Any CPU + {AEFD93B4-59B8-4B02-A694-CA15E4EC1C08}.Release|x64.Build.0 = Release|Any CPU + {AEFD93B4-59B8-4B02-A694-CA15E4EC1C08}.Release|x86.ActiveCfg = Release|Any CPU + {AEFD93B4-59B8-4B02-A694-CA15E4EC1C08}.Release|x86.Build.0 = Release|Any CPU + {C11BFE93-017D-4538-A61F-A8D98F8D2D75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C11BFE93-017D-4538-A61F-A8D98F8D2D75}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C11BFE93-017D-4538-A61F-A8D98F8D2D75}.Debug|x64.ActiveCfg = Debug|Any CPU + {C11BFE93-017D-4538-A61F-A8D98F8D2D75}.Debug|x64.Build.0 = Debug|Any CPU + {C11BFE93-017D-4538-A61F-A8D98F8D2D75}.Debug|x86.ActiveCfg = Debug|Any CPU + {C11BFE93-017D-4538-A61F-A8D98F8D2D75}.Debug|x86.Build.0 = Debug|Any CPU + {C11BFE93-017D-4538-A61F-A8D98F8D2D75}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C11BFE93-017D-4538-A61F-A8D98F8D2D75}.Release|Any CPU.Build.0 = Release|Any CPU + {C11BFE93-017D-4538-A61F-A8D98F8D2D75}.Release|x64.ActiveCfg = Release|Any CPU + {C11BFE93-017D-4538-A61F-A8D98F8D2D75}.Release|x64.Build.0 = Release|Any CPU + {C11BFE93-017D-4538-A61F-A8D98F8D2D75}.Release|x86.ActiveCfg = Release|Any CPU + {C11BFE93-017D-4538-A61F-A8D98F8D2D75}.Release|x86.Build.0 = Release|Any CPU + {38276FFF-05D8-4005-8863-018CEE833D66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38276FFF-05D8-4005-8863-018CEE833D66}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38276FFF-05D8-4005-8863-018CEE833D66}.Debug|x64.ActiveCfg = Debug|Any CPU + {38276FFF-05D8-4005-8863-018CEE833D66}.Debug|x64.Build.0 = Debug|Any CPU + {38276FFF-05D8-4005-8863-018CEE833D66}.Debug|x86.ActiveCfg = Debug|Any CPU + {38276FFF-05D8-4005-8863-018CEE833D66}.Debug|x86.Build.0 = Debug|Any CPU + {38276FFF-05D8-4005-8863-018CEE833D66}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38276FFF-05D8-4005-8863-018CEE833D66}.Release|Any CPU.Build.0 = Release|Any CPU + {38276FFF-05D8-4005-8863-018CEE833D66}.Release|x64.ActiveCfg = Release|Any CPU + {38276FFF-05D8-4005-8863-018CEE833D66}.Release|x64.Build.0 = Release|Any CPU + {38276FFF-05D8-4005-8863-018CEE833D66}.Release|x86.ActiveCfg = Release|Any CPU + {38276FFF-05D8-4005-8863-018CEE833D66}.Release|x86.Build.0 = Release|Any CPU + {32AC7906-B197-4628-9C47-9337BB5A53E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {32AC7906-B197-4628-9C47-9337BB5A53E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32AC7906-B197-4628-9C47-9337BB5A53E2}.Debug|x64.ActiveCfg = Debug|Any CPU + {32AC7906-B197-4628-9C47-9337BB5A53E2}.Debug|x64.Build.0 = Debug|Any CPU + {32AC7906-B197-4628-9C47-9337BB5A53E2}.Debug|x86.ActiveCfg = Debug|Any CPU + {32AC7906-B197-4628-9C47-9337BB5A53E2}.Debug|x86.Build.0 = Debug|Any CPU + {32AC7906-B197-4628-9C47-9337BB5A53E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {32AC7906-B197-4628-9C47-9337BB5A53E2}.Release|Any CPU.Build.0 = Release|Any CPU + {32AC7906-B197-4628-9C47-9337BB5A53E2}.Release|x64.ActiveCfg = Release|Any CPU + {32AC7906-B197-4628-9C47-9337BB5A53E2}.Release|x64.Build.0 = Release|Any CPU + {32AC7906-B197-4628-9C47-9337BB5A53E2}.Release|x86.ActiveCfg = Release|Any CPU + {32AC7906-B197-4628-9C47-9337BB5A53E2}.Release|x86.Build.0 = Release|Any CPU + {03BE4EB1-FD38-445A-9A4B-7B481560E9A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03BE4EB1-FD38-445A-9A4B-7B481560E9A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03BE4EB1-FD38-445A-9A4B-7B481560E9A2}.Debug|x64.ActiveCfg = Debug|Any CPU + {03BE4EB1-FD38-445A-9A4B-7B481560E9A2}.Debug|x64.Build.0 = Debug|Any CPU + {03BE4EB1-FD38-445A-9A4B-7B481560E9A2}.Debug|x86.ActiveCfg = Debug|Any CPU + {03BE4EB1-FD38-445A-9A4B-7B481560E9A2}.Debug|x86.Build.0 = Debug|Any CPU + {03BE4EB1-FD38-445A-9A4B-7B481560E9A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03BE4EB1-FD38-445A-9A4B-7B481560E9A2}.Release|Any CPU.Build.0 = Release|Any CPU + {03BE4EB1-FD38-445A-9A4B-7B481560E9A2}.Release|x64.ActiveCfg = Release|Any CPU + {03BE4EB1-FD38-445A-9A4B-7B481560E9A2}.Release|x64.Build.0 = Release|Any CPU + {03BE4EB1-FD38-445A-9A4B-7B481560E9A2}.Release|x86.ActiveCfg = Release|Any CPU + {03BE4EB1-FD38-445A-9A4B-7B481560E9A2}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -118,11 +406,41 @@ Global {C829F859-FF48-4025-BF22-8E63672D113C} = {C8E42992-5E42-0C2B-DBFE-AA848D06431C} {6C13E9A4-234C-4BF1-A145-F1BBA2AC56CB} = {C8E42992-5E42-0C2B-DBFE-AA848D06431C} {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {0EC62A1A-9858-3A60-8A0C-FC9AACFA2EC7} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} - {5BB2F421-C4C4-40A1-8D6E-FEBB79AE7C98} = {0EC62A1A-9858-3A60-8A0C-FC9AACFA2EC7} - {B4154CE3-9153-E2B0-0515-138FFCE32938} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} - {695CC919-E321-4DC0-8D70-C631C51624E1} = {B4154CE3-9153-E2B0-0515-138FFCE32938} - {AB2F334E-DD7C-4314-93B9-941AFC6B2ACD} = {0AB3BF05-4346-4AA6-1389-037BE0695223} - {F48A3E3A-EEBB-43AA-81AB-7DD7FE5F29B4} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {CA5186C2-289A-9324-4152-9805A73A9773} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} + {CD14770B-8BCD-4D93-AD2C-581ED030E209} = {CA5186C2-289A-9324-4152-9805A73A9773} + {10980DF6-D81A-2FEE-4ACB-4D508FA38BC6} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} + {B97352F3-709A-42CF-A448-07B9996F5FBE} = {10980DF6-D81A-2FEE-4ACB-4D508FA38BC6} + {F83653D1-5494-68B8-6444-7DEF58635BF6} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} + {BC98E6FC-B46A-4554-B75E-B912714A915C} = {F83653D1-5494-68B8-6444-7DEF58635BF6} + {AF61C489-0B09-54BC-BD16-4353135E9C4A} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} + {D052C05A-CDCD-48DF-8F4F-0B10E3719819} = {AF61C489-0B09-54BC-BD16-4353135E9C4A} + {E9D55F35-C373-3DD8-6914-3213F2F582A2} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} + {CF08AE9C-B337-47BA-B273-4B8E5610D7BD} = {E9D55F35-C373-3DD8-6914-3213F2F582A2} + {BA37DF67-98D3-4399-BD68-9679396C1547} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {AE120A0E-5815-2D46-946C-DA544028AD88} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} + {C0B5496E-F083-413E-90AE-31282A0ACCA5} = {AE120A0E-5815-2D46-946C-DA544028AD88} + {4D5D7319-366D-1BD8-49C5-A840B487E852} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} + {C905C120-DE44-4CBC-8F98-5BE148115487} = {4D5D7319-366D-1BD8-49C5-A840B487E852} + {138B168B-2473-B06D-4BD5-1B010DA319A1} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} + {F3EFD119-44F7-46BF-A03C-060F30A1A1E0} = {138B168B-2473-B06D-4BD5-1B010DA319A1} + {489D5878-3945-4F8E-AD13-E5499A18DFCC} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {E9E1155A-A339-766C-C185-6698FF423EBC} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} + {FFC12440-F128-48F5-9BE5-E5263930F876} = {E9E1155A-A339-766C-C185-6698FF423EBC} + {7A402C99-2E66-4EBE-A257-0D7923A96DFD} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {837D2CF4-7AEA-BF76-6362-47ECD7058EEC} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} + {716E089D-0661-4D39-813B-063DF9A4C0E4} = {837D2CF4-7AEA-BF76-6362-47ECD7058EEC} + {43E6E19C-39EC-49C8-91EE-49586A4B9D03} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {4BC86D3F-8D7B-ACB9-EA51-9374BC9D421A} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} + {2498C58C-53B0-4DF2-B4EA-BA69B35A7488} = {4BC86D3F-8D7B-ACB9-EA51-9374BC9D421A} + {4394178B-1ADD-478E-BF73-574B31C11292} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {2F2C7E01-5791-4A6D-9A80-C71CAD846FA8} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {76706BA9-11E0-463A-AF8E-18DFBD9E95A2} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {0587C5BB-3015-204D-19B6-EB01E03E48B6} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} + {AEFD93B4-59B8-4B02-A694-CA15E4EC1C08} = {0587C5BB-3015-204D-19B6-EB01E03E48B6} + {C11BFE93-017D-4538-A61F-A8D98F8D2D75} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {38276FFF-05D8-4005-8863-018CEE833D66} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {32AC7906-B197-4628-9C47-9337BB5A53E2} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {DD6FAF9E-10A0-95E7-0C06-698A6F70AFEE} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} + {03BE4EB1-FD38-445A-9A4B-7B481560E9A2} = {DD6FAF9E-10A0-95E7-0C06-698A6F70AFEE} EndGlobalSection EndGlobal diff --git a/backend/Tests/Knot.Modules.Admin.UnitTests/Knot.Modules.Admin.UnitTests.csproj b/backend/Tests/Knot.Modules.Admin.UnitTests/Knot.Modules.Admin.UnitTests.csproj new file mode 100644 index 0000000..d15a597 --- /dev/null +++ b/backend/Tests/Knot.Modules.Admin.UnitTests/Knot.Modules.Admin.UnitTests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Knot.Modules.Admin.UnitTests/ResetUserPasswordCommandHandlerTests.cs b/backend/Tests/Knot.Modules.Admin.UnitTests/ResetUserPasswordCommandHandlerTests.cs new file mode 100644 index 0000000..c98b46e --- /dev/null +++ b/backend/Tests/Knot.Modules.Admin.UnitTests/ResetUserPasswordCommandHandlerTests.cs @@ -0,0 +1,58 @@ +using FluentAssertions; +using Host.Application.Admin.Commands; +using Knot.Modules.Auth.Application.Abstractions; +using Knot.Modules.Auth.Domain; +using Knot.Shared.Kernel; +using NSubstitute; +using Xunit; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Knot.Modules.Admin.UnitTests; + +public class ResetUserPasswordCommandHandlerTests +{ + private readonly IAuthUnitOfWork _authUnitOfWork; + private readonly IUserRepository _userRepository; + private readonly ResetUserPasswordCommandHandler _handler; + + public ResetUserPasswordCommandHandlerTests() + { + _authUnitOfWork = Substitute.For(); + _userRepository = Substitute.For(); + _handler = new ResetUserPasswordCommandHandler(_userRepository, _authUnitOfWork); + } + + [Fact] + public async Task Handle_ShouldReturnError_WhenUserNotFound() + { + // Arrange + var command = new ResetUserPasswordCommand(Guid.NewGuid(), "NewP@ssw0rd"); + _userRepository.GetByIdAsync(command.UserId, Arg.Any()).Returns((User?)null); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsFailure.Should().BeTrue(); + result.Error.Should().Be(AuthErrors.UserNotFound); + } + + [Fact] + public async Task Handle_ShouldSucceed_WhenUserFound() + { + // Arrange + var user = User.Create("User", "pass", "salt", "admin"); + var command = new ResetUserPasswordCommand(user.Id, "NewP@ssw0rd"); + + _userRepository.GetByIdAsync(command.UserId, Arg.Any()).Returns(user); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + await _authUnitOfWork.Received(1).SaveChangesAsync(Arg.Any()); + } +} diff --git a/backend/tests/Knot.Modules.Identity.UnitTests/Knot.Modules.Identity.UnitTests.csproj b/backend/Tests/Knot.Modules.Auth.UnitTests/Knot.Modules.Auth.UnitTests.csproj similarity index 91% rename from backend/tests/Knot.Modules.Identity.UnitTests/Knot.Modules.Identity.UnitTests.csproj rename to backend/Tests/Knot.Modules.Auth.UnitTests/Knot.Modules.Auth.UnitTests.csproj index b6d5144..aea9f1e 100644 --- a/backend/tests/Knot.Modules.Identity.UnitTests/Knot.Modules.Identity.UnitTests.csproj +++ b/backend/Tests/Knot.Modules.Auth.UnitTests/Knot.Modules.Auth.UnitTests.csproj @@ -23,7 +23,9 @@ - + + + diff --git a/backend/tests/Knot.Modules.Identity.UnitTests/LoginUserCommandHandlerTests.cs b/backend/Tests/Knot.Modules.Auth.UnitTests/LoginUserCommandHandlerTests.cs similarity index 86% rename from backend/tests/Knot.Modules.Identity.UnitTests/LoginUserCommandHandlerTests.cs rename to backend/Tests/Knot.Modules.Auth.UnitTests/LoginUserCommandHandlerTests.cs index e634e8c..518e53d 100644 --- a/backend/tests/Knot.Modules.Identity.UnitTests/LoginUserCommandHandlerTests.cs +++ b/backend/Tests/Knot.Modules.Auth.UnitTests/LoginUserCommandHandlerTests.cs @@ -1,15 +1,15 @@ using FluentAssertions; using NSubstitute; -using Knot.Modules.Identity.Application.Abstractions; -using Knot.Modules.Identity.Application.Users.Login; -using Knot.Modules.Identity.Domain; +using Knot.Modules.Auth.Application.Abstractions; +using Knot.Modules.Auth.Application.Users.Login; +using Knot.Modules.Auth.Domain; using Knot.Shared.Kernel; -using Knot.Modules.Identity.Application.Users.Auth; +using Knot.Modules.Auth.Application.Users.Auth; using Xunit; using System.Threading; using System.Threading.Tasks; -namespace Knot.Modules.Identity.UnitTests; +namespace Knot.Modules.Auth.UnitTests; public class LoginUserCommandHandlerTests { @@ -61,7 +61,7 @@ public class LoginUserCommandHandlerTests // Assert result.IsFailure.Should().BeTrue(); - result.Error.Code.Should().Be(IdentityErrors.IdentityInvalidCredentials.Code); + result.Error.Code.Should().Be(AuthErrors.IdentityInvalidCredentials.Code); } [Fact] @@ -82,6 +82,7 @@ public class LoginUserCommandHandlerTests // Assert result.IsFailure.Should().BeTrue(); - result.Error.Code.Should().Be(IdentityErrors.IdentityInvalidCredentials.Code); + result.Error.Code.Should().Be(AuthErrors.IdentityInvalidCredentials.Code); } } + diff --git a/backend/tests/Knot.Modules.Chats.UnitTests/Chats/CreateChatCommandHandlerTests.cs b/backend/Tests/Knot.Modules.Conversations.UnitTests/Chats/CreateChatCommandHandlerTests.cs similarity index 85% rename from backend/tests/Knot.Modules.Chats.UnitTests/Chats/CreateChatCommandHandlerTests.cs rename to backend/Tests/Knot.Modules.Conversations.UnitTests/Chats/CreateChatCommandHandlerTests.cs index 0c56093..4de4ea6 100644 --- a/backend/tests/Knot.Modules.Chats.UnitTests/Chats/CreateChatCommandHandlerTests.cs +++ b/backend/Tests/Knot.Modules.Conversations.UnitTests/Chats/CreateChatCommandHandlerTests.cs @@ -7,11 +7,12 @@ using FluentAssertions; using NSubstitute; using Xunit; using Knot.Shared.Kernel; -using Knot.Modules.Chats.Domain; -using Knot.Modules.Chats.Application.Abstractions; -using Knot.Modules.Chats.Application.Chats.Create; +using Knot.Modules.Conversations.Domain; +using Knot.Modules.Messaging.Domain; +using Knot.Modules.Conversations.Application.Abstractions; +using Knot.Modules.Conversations.Application.Chats.Create; -namespace Knot.Modules.Chats.UnitTests.Chats; +namespace Knot.Modules.Conversations.UnitTests.Chats; public class CreateChatCommandHandlerTests { @@ -49,3 +50,5 @@ public class CreateChatCommandHandlerTests await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any()); } } + + diff --git a/backend/tests/Knot.Modules.Chats.UnitTests/Chats/GetChatsQueryHandlerTests.cs b/backend/Tests/Knot.Modules.Conversations.UnitTests/Chats/GetChatsQueryHandlerTests.cs similarity index 89% rename from backend/tests/Knot.Modules.Chats.UnitTests/Chats/GetChatsQueryHandlerTests.cs rename to backend/Tests/Knot.Modules.Conversations.UnitTests/Chats/GetChatsQueryHandlerTests.cs index aafa2db..a009178 100644 --- a/backend/tests/Knot.Modules.Chats.UnitTests/Chats/GetChatsQueryHandlerTests.cs +++ b/backend/Tests/Knot.Modules.Conversations.UnitTests/Chats/GetChatsQueryHandlerTests.cs @@ -7,12 +7,13 @@ using FluentAssertions; using NSubstitute; using Xunit; using Knot.Shared.Kernel; -using Knot.Modules.Chats.Domain; -using Knot.Modules.Chats.Application.Abstractions; -using Knot.Modules.Chats.Application.Chats.GetChats; -using Knot.Modules.Chats.Application.DTOs; +using Knot.Modules.Conversations.Domain; +using Knot.Modules.Messaging.Domain; +using Knot.Modules.Conversations.Application.Abstractions; +using Knot.Modules.Conversations.Application.Chats.GetChats; +using Knot.Modules.Conversations.Application.DTOs; -namespace Knot.Modules.Chats.UnitTests.Chats; +namespace Knot.Modules.Conversations.UnitTests.Chats; public class GetChatsQueryHandlerTests { @@ -69,3 +70,5 @@ public class GetChatsQueryHandlerTests result.Value.Any(c => c.Type == "favorites").Should().BeTrue(); } } + + diff --git a/backend/Tests/Knot.Modules.Conversations.UnitTests/CreatePersonalChatCommandHandlerTests.cs b/backend/Tests/Knot.Modules.Conversations.UnitTests/CreatePersonalChatCommandHandlerTests.cs new file mode 100644 index 0000000..a54275d --- /dev/null +++ b/backend/Tests/Knot.Modules.Conversations.UnitTests/CreatePersonalChatCommandHandlerTests.cs @@ -0,0 +1,42 @@ +using FluentAssertions; +using Knot.Modules.Conversations.Application.Chats.Commands.CreatePersonalChat; +using Knot.Modules.Conversations.Domain; +using Knot.Modules.Conversations.Application.Abstractions; +using Knot.Shared.Kernel; +using NSubstitute; +using Xunit; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Knot.Modules.Conversations.UnitTests; + +public class CreatePersonalChatCommandHandlerTests +{ + private readonly IChatRepository _chatRepository; + private readonly IChatsUnitOfWork _unitOfWork; + private readonly CreatePersonalChatCommandHandler _handler; + + public CreatePersonalChatCommandHandlerTests() + { + _chatRepository = Substitute.For(); + _unitOfWork = Substitute.For(); + _handler = new CreatePersonalChatCommandHandler(_chatRepository, _unitOfWork); + } + + [Fact] + public async Task Handle_ShouldReturnError_WhenChatAlreadyExists() + { + // Arrange + var command = new CreatePersonalChatCommand(Guid.NewGuid(), Guid.NewGuid()); + _chatRepository.GetPersonalChatAsync(command.InitiatorId, command.TargetUserId, Arg.Any()) + .Returns(PersonalChat.Create(command.InitiatorId, command.TargetUserId)); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsFailure.Should().BeTrue(); + result.Error.Should().Be(ChatErrors.PersonalChatAlreadyExists); + } +} diff --git a/backend/tests/Knot.Modules.Chats.UnitTests/GetOrCreateFavoritesCommandHandlerTests.cs b/backend/Tests/Knot.Modules.Conversations.UnitTests/GetOrCreateFavoritesCommandHandlerTests.cs similarity index 89% rename from backend/tests/Knot.Modules.Chats.UnitTests/GetOrCreateFavoritesCommandHandlerTests.cs rename to backend/Tests/Knot.Modules.Conversations.UnitTests/GetOrCreateFavoritesCommandHandlerTests.cs index 9b007da..0368f51 100644 --- a/backend/tests/Knot.Modules.Chats.UnitTests/GetOrCreateFavoritesCommandHandlerTests.cs +++ b/backend/Tests/Knot.Modules.Conversations.UnitTests/GetOrCreateFavoritesCommandHandlerTests.cs @@ -1,12 +1,13 @@ using FluentAssertions; using NSubstitute; -using Knot.Modules.Chats.Application.Abstractions; -using Knot.Modules.Chats.Application.Chats.GetOrCreateFavorites; -using Knot.Modules.Chats.Domain; +using Knot.Modules.Conversations.Application.Abstractions; +using Knot.Modules.Conversations.Application.Chats.GetOrCreateFavorites; +using Knot.Modules.Conversations.Domain; +using Knot.Modules.Messaging.Domain; using Knot.Shared.Kernel; using Xunit; -namespace Knot.Modules.Chats.UnitTests; +namespace Knot.Modules.Conversations.UnitTests; public class GetOrCreateFavoritesCommandHandlerTests { @@ -69,3 +70,5 @@ public class GetOrCreateFavoritesCommandHandlerTests await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any()); } } + + diff --git a/backend/tests/Knot.Modules.Chats.UnitTests/Knot.Modules.Chats.UnitTests.csproj b/backend/Tests/Knot.Modules.Conversations.UnitTests/Knot.Modules.Conversations.UnitTests.csproj similarity index 83% rename from backend/tests/Knot.Modules.Chats.UnitTests/Knot.Modules.Chats.UnitTests.csproj rename to backend/Tests/Knot.Modules.Conversations.UnitTests/Knot.Modules.Conversations.UnitTests.csproj index 60263e1..4939487 100644 --- a/backend/tests/Knot.Modules.Chats.UnitTests/Knot.Modules.Chats.UnitTests.csproj +++ b/backend/Tests/Knot.Modules.Conversations.UnitTests/Knot.Modules.Conversations.UnitTests.csproj @@ -23,7 +23,9 @@ - + + + diff --git a/backend/tests/Knot.Modules.Chats.UnitTests/Messages/SendMessageCommandHandlerTests.cs b/backend/Tests/Knot.Modules.Conversations.UnitTests/Messages/SendMessageCommandHandlerTests.cs similarity index 92% rename from backend/tests/Knot.Modules.Chats.UnitTests/Messages/SendMessageCommandHandlerTests.cs rename to backend/Tests/Knot.Modules.Conversations.UnitTests/Messages/SendMessageCommandHandlerTests.cs index ca25045..c40e483 100644 --- a/backend/tests/Knot.Modules.Chats.UnitTests/Messages/SendMessageCommandHandlerTests.cs +++ b/backend/Tests/Knot.Modules.Conversations.UnitTests/Messages/SendMessageCommandHandlerTests.cs @@ -8,11 +8,12 @@ using MediatR; using NSubstitute; using Xunit; using Knot.Shared.Kernel; -using Knot.Modules.Chats.Domain; -using Knot.Modules.Chats.Application.Abstractions; -using Knot.Modules.Chats.Application.Messages.Send; +using Knot.Modules.Conversations.Domain; +using Knot.Modules.Messaging.Domain; +using Knot.Modules.Conversations.Application.Abstractions; +using Knot.Modules.Conversations.Application.Messages.Send; -namespace Knot.Modules.Chats.UnitTests.Messages; +namespace Knot.Modules.Conversations.UnitTests.Messages; public class SendMessageCommandHandlerTests { @@ -95,3 +96,5 @@ public class SendMessageCommandHandlerTests await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any()); } } + + diff --git a/backend/Tests/Knot.Modules.Federation.UnitTests/Knot.Modules.Federation.UnitTests.csproj b/backend/Tests/Knot.Modules.Federation.UnitTests/Knot.Modules.Federation.UnitTests.csproj new file mode 100644 index 0000000..3b7ca74 --- /dev/null +++ b/backend/Tests/Knot.Modules.Federation.UnitTests/Knot.Modules.Federation.UnitTests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Knot.Modules.Klipy.UnitTests/Knot.Modules.Klipy.UnitTests.csproj b/backend/Tests/Knot.Modules.Klipy.UnitTests/Knot.Modules.Klipy.UnitTests.csproj new file mode 100644 index 0000000..824f599 --- /dev/null +++ b/backend/Tests/Knot.Modules.Klipy.UnitTests/Knot.Modules.Klipy.UnitTests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Knot.Modules.Messaging.UnitTests/Knot.Modules.Messaging.UnitTests.csproj b/backend/Tests/Knot.Modules.Messaging.UnitTests/Knot.Modules.Messaging.UnitTests.csproj new file mode 100644 index 0000000..6a0be6a --- /dev/null +++ b/backend/Tests/Knot.Modules.Messaging.UnitTests/Knot.Modules.Messaging.UnitTests.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + diff --git a/backend/Tests/Knot.Modules.Profiles.UnitTests/Knot.Modules.Profiles.UnitTests.csproj b/backend/Tests/Knot.Modules.Profiles.UnitTests/Knot.Modules.Profiles.UnitTests.csproj new file mode 100644 index 0000000..6a0be6a --- /dev/null +++ b/backend/Tests/Knot.Modules.Profiles.UnitTests/Knot.Modules.Profiles.UnitTests.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + diff --git a/backend/Tests/Knot.Modules.Profiles.UnitTests/UpdateProfileCommandHandlerTests.cs b/backend/Tests/Knot.Modules.Profiles.UnitTests/UpdateProfileCommandHandlerTests.cs new file mode 100644 index 0000000..ef99d04 --- /dev/null +++ b/backend/Tests/Knot.Modules.Profiles.UnitTests/UpdateProfileCommandHandlerTests.cs @@ -0,0 +1,41 @@ +using FluentAssertions; +using Knot.Modules.Profiles.Application.Profiles.Commands.UpdateProfile; +using Knot.Modules.Profiles.Domain; +using Knot.Modules.Profiles.Application.Abstractions; +using Knot.Shared.Kernel; +using NSubstitute; +using Xunit; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Knot.Modules.Profiles.UnitTests; + +public class UpdateProfileCommandHandlerTests +{ + private readonly IProfileRepository _profileRepository; + private readonly IProfilesUnitOfWork _unitOfWork; + private readonly UpdateProfileCommandHandler _handler; + + public UpdateProfileCommandHandlerTests() + { + _profileRepository = Substitute.For(); + _unitOfWork = Substitute.For(); + _handler = new UpdateProfileCommandHandler(_profileRepository, _unitOfWork); + } + + [Fact] + public async Task Handle_ShouldReturnError_WhenProfileNotFound() + { + // Arrange + var command = new UpdateProfileCommand(Guid.NewGuid(), "FirstName", "LastName", "Bio"); + _profileRepository.GetByIdAsync(command.UserId, Arg.Any()).Returns((UserProfile?)null); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsFailure.Should().BeTrue(); + result.Error.Should().Be(ProfileErrors.NotFound); + } +} diff --git a/backend/Tests/Knot.Modules.Relations.UnitTests/Knot.Modules.Relations.UnitTests.csproj b/backend/Tests/Knot.Modules.Relations.UnitTests/Knot.Modules.Relations.UnitTests.csproj new file mode 100644 index 0000000..f9ca8bf --- /dev/null +++ b/backend/Tests/Knot.Modules.Relations.UnitTests/Knot.Modules.Relations.UnitTests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Knot.Modules.Relations.UnitTests/UnitTest1.cs b/backend/Tests/Knot.Modules.Relations.UnitTests/UnitTest1.cs new file mode 100644 index 0000000..e4a5c48 --- /dev/null +++ b/backend/Tests/Knot.Modules.Relations.UnitTests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace Knot.Modules.Relations.UnitTests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} diff --git a/backend/Tests/Knot.Modules.Settings.UnitTests/Knot.Modules.Settings.UnitTests.csproj b/backend/Tests/Knot.Modules.Settings.UnitTests/Knot.Modules.Settings.UnitTests.csproj new file mode 100644 index 0000000..319b9b1 --- /dev/null +++ b/backend/Tests/Knot.Modules.Settings.UnitTests/Knot.Modules.Settings.UnitTests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Knot.Modules.Settings.UnitTests/UnitTest1.cs b/backend/Tests/Knot.Modules.Settings.UnitTests/UnitTest1.cs new file mode 100644 index 0000000..853b349 --- /dev/null +++ b/backend/Tests/Knot.Modules.Settings.UnitTests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace Knot.Modules.Settings.UnitTests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} diff --git a/backend/Tests/Knot.Modules.Storage.UnitTests/Knot.Modules.Storage.UnitTests.csproj b/backend/Tests/Knot.Modules.Storage.UnitTests/Knot.Modules.Storage.UnitTests.csproj new file mode 100644 index 0000000..bba2b3d --- /dev/null +++ b/backend/Tests/Knot.Modules.Storage.UnitTests/Knot.Modules.Storage.UnitTests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Knot.Modules.Storage.UnitTests/UnitTest1.cs b/backend/Tests/Knot.Modules.Storage.UnitTests/UnitTest1.cs new file mode 100644 index 0000000..ce94531 --- /dev/null +++ b/backend/Tests/Knot.Modules.Storage.UnitTests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace Knot.Modules.Storage.UnitTests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} diff --git a/backend/Tests/Knot.Modules.Stories.UnitTests/Knot.Modules.Stories.UnitTests.csproj b/backend/Tests/Knot.Modules.Stories.UnitTests/Knot.Modules.Stories.UnitTests.csproj new file mode 100644 index 0000000..45223ef --- /dev/null +++ b/backend/Tests/Knot.Modules.Stories.UnitTests/Knot.Modules.Stories.UnitTests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Knot.Modules.Stories.UnitTests/UnitTest1.cs b/backend/Tests/Knot.Modules.Stories.UnitTests/UnitTest1.cs new file mode 100644 index 0000000..1e2093d --- /dev/null +++ b/backend/Tests/Knot.Modules.Stories.UnitTests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace Knot.Modules.Stories.UnitTests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} diff --git a/backend/Tests/Knot.Modules.TelegramImport.UnitTests/Knot.Modules.TelegramImport.UnitTests.csproj b/backend/Tests/Knot.Modules.TelegramImport.UnitTests/Knot.Modules.TelegramImport.UnitTests.csproj new file mode 100644 index 0000000..7ab8a19 --- /dev/null +++ b/backend/Tests/Knot.Modules.TelegramImport.UnitTests/Knot.Modules.TelegramImport.UnitTests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Knot.Modules.TelegramImport.UnitTests/UnitTest1.cs b/backend/Tests/Knot.Modules.TelegramImport.UnitTests/UnitTest1.cs new file mode 100644 index 0000000..e316532 --- /dev/null +++ b/backend/Tests/Knot.Modules.TelegramImport.UnitTests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace Knot.Modules.TelegramImport.UnitTests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} diff --git a/backend/Tests/Knot.Modules.WebRtc.UnitTests/Knot.Modules.WebRtc.UnitTests.csproj b/backend/Tests/Knot.Modules.WebRtc.UnitTests/Knot.Modules.WebRtc.UnitTests.csproj new file mode 100644 index 0000000..fdc5794 --- /dev/null +++ b/backend/Tests/Knot.Modules.WebRtc.UnitTests/Knot.Modules.WebRtc.UnitTests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Knot.Modules.WebRtc.UnitTests/UnitTest1.cs b/backend/Tests/Knot.Modules.WebRtc.UnitTests/UnitTest1.cs new file mode 100644 index 0000000..c4d7b7a --- /dev/null +++ b/backend/Tests/Knot.Modules.WebRtc.UnitTests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace Knot.Modules.WebRtc.UnitTests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} diff --git a/backend/src/Host/Controllers/AdminController.cs b/backend/src/Host/Controllers/AdminController.cs deleted file mode 100644 index 73c76a4..0000000 --- a/backend/src/Host/Controllers/AdminController.cs +++ /dev/null @@ -1,110 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Host.Models; -using Knot.Shared.Kernel.Configuration; -using Knot.Modules.Identity.Application.Users.Register; -using Knot.Shared.Kernel.Storage; -using Knot.Modules.Identity.Infrastructure.Persistence; -using System.Threading; -using System.Threading.Tasks; -using System; -using MediatR; -using Host.Application.Admin.Queries; -using Host.Application.Admin.Commands; - -using Microsoft.AspNetCore.Authorization; - -namespace Host.Controllers; - -[AllowAnonymous] -[ApiController] -[Route("api/[controller]")] -public class AdminController : ControllerBase -{ - private readonly ISender _sender; - - public AdminController(ISender sender) - { - _sender = sender; - } - - [HttpGet("settings")] - public async Task GetSettings(CancellationToken ct) - { - var result = await _sender.Send(new GetSettingsQuery(), ct); - return Ok(result.Value); - } - - [HttpPut("settings")] - public async Task UpdateSettings([FromBody] SystemSettingsDto settings, CancellationToken ct) - { - var result = await _sender.Send(new UpdateSettingsCommand(settings), ct); - return Ok(result.Value); - } - - [HttpGet("dashboard")] - public async Task GetDashboardStats(CancellationToken ct) - { - var result = await _sender.Send(new GetDashboardStatsQuery(), ct); - return Ok(result.Value); - } - - [HttpPost("users")] - public async Task CreateUser([FromBody] RegisterUserCommand command, CancellationToken ct) - { - var result = await _sender.Send(command, ct); - if (result.IsFailure) return BadRequest(new { error = result.Error.Description }); - - var userDetails = await _sender.Send(new GetUserDetailsQuery(result.Value.User.Id), ct); - return Ok(userDetails.Value); - } - - [HttpPost("users/{userId:guid}/reset-password")] - public async Task ResetUserPassword(Guid userId, [FromBody] ResetPasswordDto dto, CancellationToken ct) - { - var result = await _sender.Send(new ResetUserPasswordCommand(userId, dto.NewPassword), ct); - if (result.IsFailure) - { - if (result.Error.Code == "User.NotFound") return NotFound(new { error = "User not found" }); - return BadRequest(new { error = result.Error.Description }); - } - return Ok(result.Value); - } - - [HttpGet("users")] - public async Task SearchUsers([FromQuery] string query = "", CancellationToken ct = default) - { - var result = await _sender.Send(new SearchUsersQuery(query), ct); - return Ok(result.Value); - } - - [HttpGet("users/{id:guid}")] - public async Task GetUserDetails(Guid id, CancellationToken ct) - { - var result = await _sender.Send(new GetUserDetailsQuery(id), ct); - if (result.IsFailure) - { - return NotFound("User not found"); - } - return Ok(result.Value); - } - - [HttpGet("clean/dry-run")] - public async Task CleanDryRun( - [FromServices] IFileStorageService fileStorage, - [FromServices] IdentityDbContext identityDb, - CancellationToken ct) - { - var result = await _sender.Send(new CleanDryRunQuery(fileStorage, identityDb), ct); - return Ok(result.Value); - } - - [HttpPost("clean/run")] - public async Task CleanRun( - [FromServices] IFileStorageService fileStorage, - [FromServices] IdentityDbContext identityDb, - CancellationToken ct) - { - var result = await _sender.Send(new CleanRunCommand(fileStorage, identityDb), ct); - return Ok(result.Value); - } -} \ No newline at end of file diff --git a/backend/src/Host/Controllers/AuthController.cs b/backend/src/Host/Controllers/AuthController.cs deleted file mode 100644 index b1fbe48..0000000 --- a/backend/src/Host/Controllers/AuthController.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Authorization; -using Knot.Shared.Kernel; -using Knot.Modules.Identity.Application.Users.Auth; -using Knot.Modules.Identity.Application.Users.Register; -using Knot.Modules.Identity.Application.Users.Login; -using Knot.Modules.Identity.Application.Users.GetMe; -using MediatR; - -namespace Host.Controllers; - -[ApiController] -[Route("api/[controller]")] -public sealed class AuthController : ControllerBase -{ - private readonly ISender _sender; - - public AuthController(ISender sender) - { - _sender = sender; - } - - [HttpPost("register")] - public async Task Register([FromBody] RegisterUserCommand command, CancellationToken ct) - { - var result = await _sender.Send(command, ct); - if (result.IsFailure) return BadRequest(new { error = result.Error.Code ?? result.Error.Description }); - return Ok(result.Value); - } - - [HttpGet("me")] - [Authorize] - public async Task GetMe([FromServices] IUserContext userContext, CancellationToken ct) - { - var result = await _sender.Send(new GetMeQuery(userContext.UserId), ct); - if (result.IsFailure) - { - return NotFound(); - } - return Ok(new { User = result.Value.User }); - } - - [HttpPost("login")] - public async Task Login([FromBody] LoginUserCommand command, CancellationToken ct) - { - var result = await _sender.Send(command, ct); - if (result.IsFailure) return Unauthorized(new { error = result.Error.Code ?? result.Error.Description }); - return Ok(result.Value); - } -} \ No newline at end of file diff --git a/backend/src/Host/Controllers/ChatsController.cs b/backend/src/Host/Controllers/ChatsController.cs deleted file mode 100644 index a7aadc5..0000000 --- a/backend/src/Host/Controllers/ChatsController.cs +++ /dev/null @@ -1,241 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Authorization; -using Knot.Shared.Kernel; -using Knot.Modules.Chats.Application.DTOs; -using Knot.Shared.Kernel.Storage; -using Knot.Modules.Chats.Application.Chats.GetChats; -using Knot.Modules.Chats.Application.Chats.GetChatById; -using Knot.Modules.Chats.Application.Chats.Create; -using Knot.Modules.Chats.Application.Chats.GetOrCreateFavorites; -using Knot.Modules.Chats.Application.Chats.Update; -using Knot.Modules.Chats.Application.Chats.LeaveOrDelete; -using Knot.Modules.Chats.Application.Chats.Clear; -using Knot.Modules.Chats.Application.Chats.TogglePin; -using Knot.Modules.Chats.Application.Chats.Members; -using Knot.Modules.Chats.Application.Chats.Avatar; -using Knot.Modules.Chats.Domain; -using MediatR; -using System; -using System.Linq; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Host.Controllers; - -[Authorize] -[ApiController] -[Route("api/chats")] -public sealed class ChatsController : ControllerBase -{ - private readonly ISender _sender; - private readonly IUserContext _userContext; - - public ChatsController(ISender sender, IUserContext userContext) - { - _sender = sender; - _userContext = userContext; - } - - [HttpGet] - public async Task GetChats(CancellationToken ct) - { - var result = await _sender.Send(new GetChatsQuery(_userContext.UserId), ct); - if (result.IsFailure) - { - return BadRequest(result.Error.Description); - } - return Ok(result.Value); - } - - [HttpPost] - public async Task Create([FromBody] CreateChatRequest request, CancellationToken ct) - { - var command = new CreateChatCommand(request.Name, request.Type, request.MemberIds); - var result = await _sender.Send(command, ct); - if (result.IsFailure) - { - return BadRequest(result.Error.Description); - } - - var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct); - return Ok(chatResult.Value); - } - - [HttpPost("personal")] - public async Task CreatePersonal([FromBody] CreatePersonalChatRequest request, CancellationToken ct) - { - var command = new CreateChatCommand(string.Empty, ChatType.Personal, new List { _userContext.UserId, request.UserId }); - var result = await _sender.Send(command, ct); - if (result.IsFailure) - { - return BadRequest(result.Error.Description); - } - - var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct); - return Ok(chatResult.Value); - } - - [HttpPost("group")] - public async Task CreateGroup([FromBody] CreateGroupChatRequest request, CancellationToken ct) - { - var memberIds = request.MemberIds.ToList(); - if (memberIds.Contains(_userContext.UserId)) - { - memberIds.Remove(_userContext.UserId); - } - memberIds.Insert(0, _userContext.UserId); - - var command = new CreateChatCommand(request.Name, ChatType.Group, memberIds); - var result = await _sender.Send(command, ct); - if (result.IsFailure) - { - return BadRequest(result.Error.Description); - } - - var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct); - return Ok(chatResult.Value); - } - - [HttpPost("favorites")] - public async Task GetOrCreateFavorites(CancellationToken ct) - { - var result = await _sender.Send(new GetOrCreateFavoritesCommand(_userContext.UserId), ct); - if (result.IsFailure) - { - return BadRequest(result.Error.Description); - } - - var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct); - return Ok(chatResult.Value); - } - - [HttpPut("{id:guid}")] - public async Task UpdateChat(Guid id, [FromBody] UpdateChatRequest request, CancellationToken ct) - { - var result = await _sender.Send(new UpdateChatCommand(id, _userContext.UserId, request.Name, request.Description), ct); - if (result.IsFailure) - { - return NotFound(); - } - - var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct); - return Ok(chatResult.Value); - } - - [HttpDelete("{id:guid}")] - public async Task LeaveOrDeleteChat(Guid id, CancellationToken ct) - { - var result = await _sender.Send(new LeaveOrDeleteChatCommand(id, _userContext.UserId), ct); - if (result.IsFailure) - { - if (result.Error.Code == "Unauthorized") - { - return Forbid(); - } - return NotFound(); - } - return Ok(result.Value); - } - - [HttpPost("{id:guid}/clear")] - public async Task ClearChat(Guid id, CancellationToken ct) - { - var result = await _sender.Send(new ClearChatCommand(id, _userContext.UserId), ct); - if (result.IsFailure) - { - return NotFound(); - } - return Ok(result.Value); - } - - [HttpPost("{id:guid}/pin")] - public async Task TogglePin(Guid id, CancellationToken ct) - { - var result = await _sender.Send(new TogglePinCommand(id, _userContext.UserId), ct); - if (result.IsFailure) - { - return NotFound(); - } - return Ok(result.Value); - } - - [HttpPost("{id:guid}/members")] - public async Task AddMembers(Guid id, [FromBody] AddMembersRequest request, CancellationToken ct) - { - var result = await _sender.Send(new AddMembersCommand(id, _userContext.UserId, request.UserIds.ToList()), ct); - if (result.IsFailure) - { - return NotFound(); - } - - var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct); - return Ok(chatResult.Value); - } - - [HttpDelete("{id:guid}/members/{userId:guid}")] - public async Task RemoveMember(Guid id, Guid userId, CancellationToken ct) - { - var result = await _sender.Send(new RemoveMemberCommand(id, _userContext.UserId, userId), ct); - if (result.IsFailure) - { - return NotFound(); - } - - var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct); - return Ok(chatResult.Value); - } - - [HttpPost("{id:guid}/avatar")] - public async Task UploadGroupAvatar(Guid id, Microsoft.AspNetCore.Http.IFormFile avatar, CancellationToken ct) - { - if (avatar == null || avatar.Length == 0) - { - return BadRequest("No file"); - } - - using var stream = avatar.OpenReadStream(); - var result = await _sender.Send(new UploadGroupAvatarCommand(id, _userContext.UserId, avatar.FileName, avatar.ContentType, stream), ct); - - if (result.IsFailure) - { - return NotFound(); - } - - var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct); - return Ok(chatResult.Value); - } - - [HttpPost("{id:guid}/avatar/crop")] - public async Task CropGroupAvatar(Guid id, [FromForm] Microsoft.AspNetCore.Http.IFormFile avatar, [FromForm] int x, [FromForm] int y, [FromForm] int width, [FromForm] int height, CancellationToken ct) - { - if (avatar == null || avatar.Length == 0) - { - return BadRequest("No file"); - } - - using var stream = avatar.OpenReadStream(); - var result = await _sender.Send(new CropGroupAvatarCommand(id, _userContext.UserId, avatar.FileName, avatar.ContentType, stream, x, y, width, height), ct); - - if (result.IsFailure) - { - return NotFound(); - } - - var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct); - return Ok(chatResult.Value); - } - - [HttpDelete("{id:guid}/avatar")] - public async Task RemoveGroupAvatar(Guid id, CancellationToken ct) - { - var result = await _sender.Send(new RemoveGroupAvatarCommand(id, _userContext.UserId), ct); - if (result.IsFailure) - { - return NotFound(); - } - - var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct); - return Ok(chatResult.Value); - } -} \ No newline at end of file diff --git a/backend/src/Host/Controllers/FilesController.cs b/backend/src/Host/Controllers/FilesController.cs deleted file mode 100644 index 3f808c4..0000000 --- a/backend/src/Host/Controllers/FilesController.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Authorization; -using Knot.Shared.Kernel.Storage; - -namespace Host.Controllers; - -[Authorize] -[ApiController] -[Route("api/files")] -public sealed class FilesController : ControllerBase -{ - private readonly IFileStorageService _fileStorage; - - public FilesController(IFileStorageService fileStorage) - { - _fileStorage = fileStorage; - } - - [HttpGet("{id}")] - [AllowAnonymous] - public async Task DownloadFile(string id, [FromQuery] bool download = false) - { - try - { - var result = await _fileStorage.DownloadFileAsync(id); - if (download && !string.IsNullOrEmpty(result.FileName)) - { - return File(result.Stream, result.ContentType, result.FileName, enableRangeProcessing: true); - } - - return File(result.Stream, result.ContentType, enableRangeProcessing: true); - } - catch (Exception ex) - { - return NotFound(new { error = "Файл не найден или ошибка доступа.", message = ex.Message }); - } - } -} diff --git a/backend/src/Host/Controllers/FriendsController.cs b/backend/src/Host/Controllers/FriendsController.cs deleted file mode 100644 index af0da69..0000000 --- a/backend/src/Host/Controllers/FriendsController.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Knot.Modules.Identity.Application.Friends; -using Knot.Shared.Kernel; -using MediatR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace Host.Controllers; - -[Authorize] -[ApiController] -[Route("api/friends")] -public sealed class FriendsController : ControllerBase -{ - private readonly ISender _sender; - private readonly IUserContext _userContext; - - public FriendsController(ISender sender, IUserContext userContext) - { - _sender = sender; - _userContext = userContext; - } - - [HttpGet] - public async Task GetFriends(CancellationToken ct) - { - var result = await _sender.Send(new GetFriendsQuery(_userContext.UserId), ct); - if (result.IsFailure) - { - return BadRequest(result.Error); - } - return Ok(result.Value); - } - - [HttpGet("requests")] - public async Task GetRequests(CancellationToken ct) - { - var result = await _sender.Send(new GetIncomingRequestsQuery(_userContext.UserId), ct); - if (result.IsFailure) - { - return BadRequest(result.Error); - } - return Ok(result.Value); - } - - [HttpGet("outgoing")] - public async Task GetOutgoingRequests(CancellationToken ct) - { - var result = await _sender.Send(new GetOutgoingRequestsQuery(_userContext.UserId), ct); - if (result.IsFailure) - { - return BadRequest(result.Error); - } - return Ok(result.Value); - } - - [HttpPost("request")] - public async Task SendRequest([FromBody] Host.Models.SendFriendRequest request, CancellationToken ct) - { - var result = await _sender.Send(new SendFriendRequestCommand(_userContext.UserId, request.FriendId), ct); - if (result.IsFailure) - { - return BadRequest(result.Error.Description); - } - return Ok(new { status = "pending" }); - } - - [HttpPost("{id:guid}/accept")] - public async Task AcceptRequest(Guid id, CancellationToken ct) - { - var result = await _sender.Send(new AcceptFriendRequestCommand(_userContext.UserId, id), ct); - if (result.IsFailure) - { - return NotFound(result.Error.Description); - } - return Ok(new { id = result.Value }); - } - - [HttpPost("{id:guid}/decline")] - public async Task DeclineRequest(Guid id, CancellationToken ct) - { - var result = await _sender.Send(new DeclineFriendRequestCommand(_userContext.UserId, id), ct); - if (result.IsFailure) - { - return NotFound(result.Error.Description); - } - return Ok(new Knot.Shared.Kernel.SuccessResponse(true)); - } - - [HttpGet("status/{userId:guid}")] - public async Task GetStatus(Guid userId, CancellationToken ct) - { - var result = await _sender.Send(new GetFriendshipStatusQuery(_userContext.UserId, userId), ct); - if (result.IsFailure) - { - return BadRequest(result.Error); - } - return Ok(result.Value); - } - - [HttpDelete("{id:guid}")] - public async Task RemoveFriend(Guid id, CancellationToken ct) - { - var result = await _sender.Send(new RemoveFriendCommand(_userContext.UserId, id), ct); - if (result.IsFailure) - { - return NotFound(result.Error.Description); - } - return Ok(new Knot.Shared.Kernel.SuccessResponse(true)); - } -} \ No newline at end of file diff --git a/backend/src/Host/Controllers/MessagesController.cs b/backend/src/Host/Controllers/MessagesController.cs deleted file mode 100644 index 466c776..0000000 --- a/backend/src/Host/Controllers/MessagesController.cs +++ /dev/null @@ -1,116 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Authorization; -using Knot.Modules.Chats.Application.DTOs; -using Knot.Shared.Kernel; -using System.Threading; -using System.Threading.Tasks; -using System; -using Microsoft.AspNetCore.Http; -using MediatR; -using Knot.Modules.Chats.Application.Messages.GetMessages; -using Knot.Modules.Chats.Application.Messages.SearchMessages; -using Knot.Modules.Chats.Application.Messages.UploadFile; -using Knot.Modules.Chats.Application.Messages.GetSharedMedia; -using Knot.Modules.Chats.Application.Messages.Send; -using System.Linq; - -namespace Host.Controllers; - -[Authorize] -[ApiController] -[Route("api/messages")] -public sealed class MessagesController : ControllerBase -{ - private readonly ISender _sender; - private readonly IUserContext _userContext; - - public MessagesController(ISender sender, IUserContext userContext) - { - _sender = sender; - _userContext = userContext; - } - - [HttpGet("chat/{chatId:guid}")] - public async Task GetMessages(Guid chatId, [FromQuery] string? cursor, CancellationToken ct = default) - { - var result = await _sender.Send(new GetMessagesQuery(_userContext.UserId, chatId, cursor), ct); - if (result.IsFailure) - { - return BadRequest(result.Error.Description); - } - return Ok(result.Value); - } - - [HttpGet("search")] - public async Task GetSearch([FromQuery] string q, [FromQuery] Guid? chatId, CancellationToken ct) - { - var result = await _sender.Send(new SearchMessagesQuery(_userContext.UserId, q, chatId), ct); - if (result.IsFailure) - { - return BadRequest(result.Error.Description); - } - return Ok(result.Value); - } - - [HttpPost("upload")] - [DisableRequestSizeLimit] - [RequestFormLimits(MultipartBodyLengthLimit = 10L * 1024 * 1024 * 1024)] // 10 GB limit for form body - public async Task UploadFile(IFormFile file, CancellationToken ct) - { - if (file == null || file.Length == 0) - { - return BadRequest("No file uploaded"); - } - - using var stream = file.OpenReadStream(); - var result = await _sender.Send(new UploadFileCommand(file.FileName, file.ContentType, file.Length, stream), ct); - - if (result.IsFailure) - { - if (result.Error.Code == "File.TooLarge") - { - return StatusCode(413, result.Error.Description); - } - return BadRequest(result.Error.Description); - } - - return Ok(result.Value); - } - - [HttpGet("chat/{chatId:guid}/shared")] - public async Task GetSharedMedia(Guid chatId, [FromQuery] string? type, CancellationToken ct) - { - var result = await _sender.Send(new GetSharedMediaQuery(_userContext.UserId, chatId, type), ct); - if (result.IsFailure) - { - return BadRequest(result.Error.Description); - } - return Ok(result.Value); - } - - [HttpPost("chat/{chatId:guid}")] - public async Task SendMessage(Guid chatId, [FromBody] SendMessageRequest request, CancellationToken ct) - { - var attachments = request.Attachments?.Select(a => - new AttachmentRequest(a.Type, a.Url, a.FileName, a.FileSize)).ToList(); - - var command = new SendMessageCommand( - chatId, - _userContext.UserId, - request.Content, - request.Type, - attachments, - request.ReplyToId, - request.Quote, - request.ForwardedFromId); - - var result = await _sender.Send(command, ct); - - if (result.IsFailure) - { - return BadRequest(result.Error.Description); - } - - return Ok(result.Value); - } -} \ No newline at end of file diff --git a/backend/src/Host/Controllers/StoriesController.cs b/backend/src/Host/Controllers/StoriesController.cs deleted file mode 100644 index 3ea3174..0000000 --- a/backend/src/Host/Controllers/StoriesController.cs +++ /dev/null @@ -1,149 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Authorization; -using Host.Models; -using Knot.Shared.Kernel; -using System; -using System.Threading; -using System.Threading.Tasks; -using MediatR; -using Host.Application.Stories.Queries.GetStories; -using Host.Application.Stories.Commands.CreateStory; -using Host.Application.Stories.Queries.GetUserStories; -using Host.Application.Stories.Commands.ViewStory; -using Host.Application.Stories.Queries.GetStoryViewers; -using Host.Application.Stories.Commands.AddStoryReaction; -using Host.Application.Stories.Commands.RemoveStoryReaction; -using Host.Application.Stories.Commands.AddStoryReply; -using Host.Application.Stories.Queries.GetStoryReplies; -using Host.Application.Stories.Commands.DeleteStory; - -namespace Host.Controllers; - -[Authorize] -[ApiController] -[Route("api/stories")] -public sealed class StoriesController : ControllerBase -{ - private readonly ISender _sender; - private readonly IUserContext _userContext; - - public StoriesController(ISender sender, IUserContext userContext) - { - _sender = sender; - _userContext = userContext; - } - - [HttpGet] - public async Task GetStories(CancellationToken ct) - { - var result = await _sender.Send(new GetStoriesQuery(_userContext.UserId), ct); - return Ok(result.Value); - } - - [HttpPost] - public async Task CreateStory([FromBody] CreateStoryRequest request, CancellationToken ct) - { - var result = await _sender.Send(new CreateStoryCommand(_userContext.UserId, request.Type, request.MediaUrl, request.Content, request.BgColor), ct); - return Ok(new { id = result.Value }); - } - - [HttpGet("user/{userId}")] - public async Task GetUserStories(Guid userId, CancellationToken ct) - { - var result = await _sender.Send(new GetUserStoriesQuery(_userContext.UserId, userId), ct); - if (result.IsFailure) - { - return NotFound(); - } - return Ok(result.Value); - } - - [HttpPost("{id}/view")] - public async Task ViewStory(Guid id, CancellationToken ct) - { - var result = await _sender.Send(new ViewStoryCommand(_userContext.UserId, id), ct); - if (result.IsFailure) - { - return NotFound(); - } - return Ok(result.Value); - } - - [HttpGet("{id}/viewers")] - public async Task GetStoryViewers(Guid id, CancellationToken ct) - { - var result = await _sender.Send(new GetStoryViewersQuery(_userContext.UserId, id), ct); - if (result.IsFailure) - { - if (result.Error.Code == "Unauthorized") - { - return Forbid(); - } - return NotFound(); - } - return Ok(result.Value); - } - - [HttpPost("{id}/reaction")] - public async Task AddReaction(Guid id, [FromBody] AddStoryReactionRequest request, CancellationToken ct) - { - var result = await _sender.Send(new AddStoryReactionCommand(_userContext.UserId, id, request.Emoji), ct); - if (result.IsFailure) - { - return NotFound(); - } - return Ok(result.Value); - } - - [HttpDelete("{id}/reaction")] - public async Task RemoveReaction(Guid id, [FromBody] RemoveStoryReactionRequest request, CancellationToken ct) - { - var result = await _sender.Send(new RemoveStoryReactionCommand(_userContext.UserId, id, request.Emoji), ct); - if (result.IsFailure) - { - return NotFound(); - } - return Ok(result.Value); - } - - [HttpPost("{id}/reply")] - public async Task AddReply(Guid id, [FromBody] AddStoryReplyRequest request, CancellationToken ct) - { - var result = await _sender.Send(new AddStoryReplyCommand(_userContext.UserId, id, request.Content), ct); - if (result.IsFailure) - { - return NotFound(); - } - return Ok(result.Value); - } - - [HttpGet("{id}/replies")] - public async Task GetReplies(Guid id, CancellationToken ct) - { - var result = await _sender.Send(new GetStoryRepliesQuery(_userContext.UserId, id), ct); - if (result.IsFailure) - { - if (result.Error.Code == "Unauthorized") - { - return Forbid(); - } - return NotFound(); - } - return Ok(result.Value); - } - - [HttpDelete("{id}")] - public async Task DeleteStory(Guid id, CancellationToken ct) - { - var result = await _sender.Send(new DeleteStoryCommand(_userContext.UserId, id), ct); - if (result.IsFailure) - { - if (result.Error.Code == "Unauthorized") - { - return Forbid(); - } - return NotFound(); - } - return Ok(result.Value); - } -} \ No newline at end of file diff --git a/backend/src/Host/Controllers/TelegramImportController.cs b/backend/src/Host/Controllers/TelegramImportController.cs deleted file mode 100644 index defcf1b..0000000 --- a/backend/src/Host/Controllers/TelegramImportController.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using MediatR; -using Host.Models; -using Knot.Shared.Kernel; -using Knot.Modules.Chats.Application.TelegramImport; -using System.Threading; -using System.Threading.Tasks; - -namespace Host.Controllers; - -[Authorize] -[ApiController] -[Route("api/import/telegram")] -public sealed class TelegramImportController : ControllerBase -{ - private readonly ISender _sender; - private readonly IUserContext _userContext; - - public TelegramImportController(ISender sender, IUserContext userContext) - { - _sender = sender; - _userContext = userContext; - } - - [HttpPost("analyze")] - [DisableRequestSizeLimit] - [RequestFormLimits(MultipartBodyLengthLimit = 10L * 1024 * 1024 * 1024)] // 10GB for big exports - public async Task Analyze(IFormFile file, CancellationToken ct) - { - if (file == null) - { - return BadRequest("No file uploaded."); - } - - using var stream = file.OpenReadStream(); - var command = new AnalyzeImportCommand(stream, file.FileName); - - var result = await _sender.Send(command, ct); - - if (result.IsFailure) - { - return BadRequest(result.Error.Description ?? result.Error.Code); - } - - return Ok(result.Value); - } - - [HttpPost("execute")] - public async Task Execute([FromBody] ExecuteImportRequest req, CancellationToken ct) - { - var command = new ExecuteImportCommand(_userContext.UserId, req.Token, req.Mapping, req.GroupName); - - var result = await _sender.Send(command, ct); - - if (result.IsFailure) - { - return BadRequest(result.Error.Description ?? result.Error.Code); - } - - return Ok(result.Value); - } -} diff --git a/backend/src/Host/Controllers/UsersController.cs b/backend/src/Host/Controllers/UsersController.cs deleted file mode 100644 index ac68fd8..0000000 --- a/backend/src/Host/Controllers/UsersController.cs +++ /dev/null @@ -1,134 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Authorization; -using Knot.Shared.Kernel; -using Host.Models; -using Knot.Shared.Kernel.Storage; -using MediatR; -using Knot.Modules.Identity.Application.Users; -using Knot.Modules.Identity.Application.Users.Avatar; -using Knot.Modules.Identity.Application.Users.GetUser; -using Knot.Modules.Identity.Application.Users.Search; -using Knot.Modules.Identity.Application.Users.UpdateProfile; -using Knot.Modules.Identity.Application.Users.UpdateSettings; - -namespace Host.Controllers; - -[Authorize] -[ApiController] -[Route("api/users")] -public sealed class UsersController : ControllerBase -{ - private readonly ISender _sender; - private readonly IUserContext _userContext; - - public UsersController(ISender sender, IUserContext userContext) - { - _sender = sender; - _userContext = userContext; - } - - [HttpGet("search")] - public async Task Search([FromQuery] string q, CancellationToken ct) - { - var result = await _sender.Send(new SearchUsersQuery(q), ct); - if (result.IsFailure) - { - return BadRequest(result.Error); - } - return Ok(result.Value); - } - - [HttpPut("settings")] - public async Task UpdateSettings([FromBody] UpdateSettingsRequest request, CancellationToken ct) - { - var result = await _sender.Send(new UpdateSettingsCommand(_userContext.UserId, request.HideStoryViews), ct); - if (result.IsFailure) - { - return NotFound(result.Error); - } - return Ok(result.Value); - } - - [HttpPost("avatar")] - public async Task UploadAvatar(IFormFile avatar, CancellationToken ct) - { - var fileToUpload = avatar ?? Request.Form.Files.FirstOrDefault(); - if (fileToUpload == null || fileToUpload.Length == 0) - { - return BadRequest("No file uploaded"); - } - - using var stream = fileToUpload.OpenReadStream(); - var command = new UploadAvatarCommand( - _userContext.UserId, - stream, - fileToUpload.FileName, - fileToUpload.ContentType - ); - - var result = await _sender.Send(command, ct); - if (result.IsFailure) - { - return NotFound(result.Error); - } - return Ok(result.Value); - } - - [HttpPost("avatar/crop")] - public async Task CropAvatar([FromForm] IFormFile avatar, [FromForm] int x, [FromForm] int y, [FromForm] int width, [FromForm] int height, CancellationToken ct) - { - if (avatar == null || avatar.Length == 0) - { - return BadRequest("No file uploaded"); - } - - using var stream = avatar.OpenReadStream(); - var command = new CropAvatarCommand( - _userContext.UserId, - stream, - avatar.FileName ?? "avatar.jpg", - avatar.ContentType, - x, y, width, height - ); - - var result = await _sender.Send(command, ct); - if (result.IsFailure) - { - return NotFound(result.Error); - } - return Ok(result.Value); - } - - [HttpDelete("avatar")] - public async Task DeleteAvatar(CancellationToken ct) - { - var result = await _sender.Send(new DeleteAvatarCommand(_userContext.UserId), ct); - if (result.IsFailure) - { - return NotFound(result.Error); - } - return Ok(result.Value); - } - - [HttpPut("profile")] - public async Task UpdateProfile([FromBody] UpdateProfileRequest request, CancellationToken ct) - { - var result = await _sender.Send(new UpdateProfileCommand(_userContext.UserId, request.DisplayName, request.Bio, request.Birthday), ct); - if (result.IsFailure) - { - return NotFound(result.Error); - } - return Ok(result.Value); - } - - [HttpGet("{id:guid}")] - public async Task GetUser(Guid id, CancellationToken ct) - { - var result = await _sender.Send(new GetUserQuery(id), ct); - if (result.IsFailure) - { - return NotFound(result.Error); - } - return Ok(result.Value); - } -} \ No newline at end of file diff --git a/backend/src/Host/Endpoints/ConfigEndpoints.cs b/backend/src/Host/Endpoints/ConfigEndpoints.cs deleted file mode 100644 index c00df8b..0000000 --- a/backend/src/Host/Endpoints/ConfigEndpoints.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Carter; -using MediatR; -using Knot.Shared.Kernel.Constants; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; - -namespace Host.Endpoints; - -/// -/// Регистрация эндпоинтов для конфигурации приложения. -/// -public sealed class ConfigEndpoints : ICarterModule -{ - public void AddRoutes(IEndpointRouteBuilder app) - { - var group = app.MapGroup(Routes.ApiConfig); - - group.MapGet("/", async (ISender sender, CancellationToken ct) => - { - var result = await sender.Send(new Host.Application.Config.Queries.GetPublicConfigQuery(), ct); - return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error); - }); - } -} diff --git a/backend/src/Host/Endpoints/FederationEndpoints.cs b/backend/src/Host/Endpoints/FederationEndpoints.cs deleted file mode 100644 index b8e8276..0000000 --- a/backend/src/Host/Endpoints/FederationEndpoints.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Carter; -using MediatR; -using Knot.Shared.Kernel.Constants; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using System; - -namespace Host.Endpoints; - -/// -/// Регистрация эндпоинтов федерации. -/// -public sealed class FederationEndpoints : ICarterModule -{ - public void AddRoutes(IEndpointRouteBuilder app) - { - var group = app.MapGroup(Routes.ApiFederation); - - group.MapPost("/handshake", async ([FromBody] Host.Application.Federation.Commands.HandshakeRequest request, ISender sender, CancellationToken ct) => - { - var result = await sender.Send(new Host.Application.Federation.Commands.HandshakeFederationCommand(request), ct); - if (result.IsSuccess) - { - return Results.Ok(result.Value); - } - - if (result.Error.Code == "Unauthorized") - { - return Results.Forbid(); - } - if (result.Error.Code == Knot.Shared.Kernel.Constants.Errors.DisabledByAdmin) - { - return Results.StatusCode(503); - } - - return Results.BadRequest(new { error = result.Error.Description }); - }); - } -} \ No newline at end of file diff --git a/backend/src/Host/Endpoints/WebRtcEndpoints.cs b/backend/src/Host/Endpoints/WebRtcEndpoints.cs deleted file mode 100644 index f121727..0000000 --- a/backend/src/Host/Endpoints/WebRtcEndpoints.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Carter; -using MediatR; -using Knot.Shared.Kernel; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; - -namespace Host.Endpoints; - -/// -/// Регистрация эндпоинтов WebRTC -/// -public sealed class WebRtcEndpoints : ICarterModule -{ - public void AddRoutes(IEndpointRouteBuilder app) - { - var group = app.MapGroup(Knot.Shared.Kernel.Constants.Routes.ApiWebRtc) - .RequireAuthorization(); - - group.MapGet("/ice-servers", async (ISender sender, CancellationToken ct) => - { - var result = await sender.Send(new Host.Application.WebRtc.Queries.GetIceServersQuery(), ct); - return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error); - }) - .WithName("GetIceServers") - .WithOpenApi(); - } -} diff --git a/backend/src/Host/Host.csproj b/backend/src/Host/Host.csproj index d753cb1..cf96201 100644 --- a/backend/src/Host/Host.csproj +++ b/backend/src/Host/Host.csproj @@ -28,9 +28,18 @@ - - + + + + + + + + + + + diff --git a/backend/src/Host/Models/Friends/SendFriendRequest.cs b/backend/src/Host/Models/Friends/SendFriendRequest.cs deleted file mode 100644 index 382ea0e..0000000 --- a/backend/src/Host/Models/Friends/SendFriendRequest.cs +++ /dev/null @@ -1,5 +0,0 @@ -using System; - -namespace Host.Models; - -public record SendFriendRequest(Guid FriendId); diff --git a/backend/src/Host/Models/Stories/AddStoryReactionRequest.cs b/backend/src/Host/Models/Stories/AddStoryReactionRequest.cs deleted file mode 100644 index 0ff9548..0000000 --- a/backend/src/Host/Models/Stories/AddStoryReactionRequest.cs +++ /dev/null @@ -1,5 +0,0 @@ -using System; - -namespace Host.Models; - -public record AddStoryReactionRequest(string Emoji); diff --git a/backend/src/Host/Models/Stories/AddStoryReplyRequest.cs b/backend/src/Host/Models/Stories/AddStoryReplyRequest.cs deleted file mode 100644 index 5c4d2cc..0000000 --- a/backend/src/Host/Models/Stories/AddStoryReplyRequest.cs +++ /dev/null @@ -1,5 +0,0 @@ -using System; - -namespace Host.Models; - -public record AddStoryReplyRequest(string Content); diff --git a/backend/src/Host/Models/Stories/RemoveStoryReactionRequest.cs b/backend/src/Host/Models/Stories/RemoveStoryReactionRequest.cs deleted file mode 100644 index f011f5e..0000000 --- a/backend/src/Host/Models/Stories/RemoveStoryReactionRequest.cs +++ /dev/null @@ -1,5 +0,0 @@ -using System; - -namespace Host.Models; - -public record RemoveStoryReactionRequest(string Emoji); diff --git a/backend/src/Host/Models/Users/UpdateProfileRequest.cs b/backend/src/Host/Models/Users/UpdateProfileRequest.cs deleted file mode 100644 index 160eb9c..0000000 --- a/backend/src/Host/Models/Users/UpdateProfileRequest.cs +++ /dev/null @@ -1,5 +0,0 @@ -using System; - -namespace Host.Models; - -public sealed record UpdateProfileRequest(string? DisplayName, string? Bio, DateTime? Birthday); diff --git a/backend/src/Host/Models/Users/UpdateSettingsRequest.cs b/backend/src/Host/Models/Users/UpdateSettingsRequest.cs deleted file mode 100644 index 188ab7a..0000000 --- a/backend/src/Host/Models/Users/UpdateSettingsRequest.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Host.Models; - -public sealed record UpdateSettingsRequest(bool? HideStoryViews); diff --git a/backend/src/Host/Models/Users/UserDto.cs b/backend/src/Host/Models/Users/UserDto.cs deleted file mode 100644 index b0c62a7..0000000 --- a/backend/src/Host/Models/Users/UserDto.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Host.Models; - -public record UserDto( - Guid Id, - string Username, - string DisplayName, - string? Avatar, - bool IsOnline, - DateTime LastSeen -); diff --git a/backend/src/Host/Models/Users/UserProfileDto.cs b/backend/src/Host/Models/Users/UserProfileDto.cs deleted file mode 100644 index 7a1682b..0000000 --- a/backend/src/Host/Models/Users/UserProfileDto.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Host.Models; - -public record UserProfileDto( - Guid Id, - string Username, - string DisplayName, - string? Avatar, - string? Bio, - DateTime? Birthday, - DateTime CreatedAt, - bool? HideStoryViews = null, - bool IsOnline = false, - DateTime? LastSeen = null -); diff --git a/backend/src/Host/Program.cs b/backend/src/Host/Program.cs index 6ae34a4..71f7da1 100644 --- a/backend/src/Host/Program.cs +++ b/backend/src/Host/Program.cs @@ -1,10 +1,11 @@ +using Knot.Modules.Messaging; using Carter; using Knot.Shared.Infrastructure; -using Knot.Modules.Identity; -using Knot.Modules.Chats; -using Knot.Modules.Chats.Infrastructure.SignalR; -using Knot.Modules.Identity.Infrastructure.Persistence; -using Knot.Modules.Chats.Infrastructure.Persistence; +using Knot.Modules.Auth; +using Knot.Modules.Conversations; +using Knot.Modules.Conversations.Infrastructure.SignalR; +using Knot.Modules.Auth.Infrastructure.Persistence; +using Knot.Modules.Conversations.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; @@ -26,7 +27,7 @@ var builder = WebApplication.CreateBuilder(args); // Маппинг стандартных переменных окружения в иерархию .NET var envMappings = new Dictionary { - ["ConnectionStrings:DefaultConnection"] = builder.Configuration["DATABASE_URL"], + ["ConnectionStrings:DefaultConnection"] = builder.Configuration["DATABASE_URL"] ?? "Host=localhost;Database=knot;Username=postgres;Password=postgres", ["ConnectionStrings:MongoConnection"] = builder.Configuration["MONGO_CONNECTION"], ["Jwt:Secret"] = builder.Configuration["JWT_SECRET"], ["Jwt:Issuer"] = builder.Configuration["JWT_ISSUER"], @@ -48,8 +49,8 @@ builder.Configuration.AddInMemoryCollection( envMappings.Where(kv => !string.IsNullOrEmpty(kv.Value)) .ToDictionary(kv => kv.Key, kv => kv.Value)); -builder.Services.AddIdentityModule(builder.Configuration); -builder.Services.AddChatsModule(builder.Configuration); +builder.Services.AddAuthModule(builder.Configuration); +builder.Services.AddConversationsModule(builder.Configuration); builder.Services.AddSharedInfrastructure(builder.Configuration); // CQRS / MediatR для команд в Host (например, AdminController) @@ -98,12 +99,7 @@ builder.Services.AddRouting(options => builder.Services.AddMemoryCache(); builder.Services.AddHttpClient(); -builder.Services.AddControllers() - .AddJsonOptions(options => - { - options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - }); + builder.Services.AddSignalR() .AddJsonProtocol(options => @@ -152,10 +148,10 @@ var app = builder.Build(); // Применяем миграции при старте using (var scope = app.Services.CreateScope()) { - var identityDb = scope.ServiceProvider.GetRequiredService(); + var identityDb = scope.ServiceProvider.GetRequiredService(); await identityDb.Database.MigrateAsync(); - var chatsDb = scope.ServiceProvider.GetRequiredService(); + var chatsDb = scope.ServiceProvider.GetRequiredService(); await chatsDb.Database.MigrateAsync(); var systemDb = scope.ServiceProvider.GetRequiredService(); @@ -163,7 +159,7 @@ using (var scope = app.Services.CreateScope()) // Set Encryption Service for MongoDB serializers var encryptionService = scope.ServiceProvider.GetRequiredService(); - Knot.Modules.Chats.Infrastructure.Persistence.Mongo.EncryptedStringSerializer.EncryptionService = encryptionService; + Knot.Modules.Messaging.Infrastructure.Persistence.Mongo.EncryptedStringSerializer.EncryptionService = encryptionService; // Initialize Global Settings Cache var settingsService = scope.ServiceProvider.GetRequiredService(); @@ -193,7 +189,6 @@ app.UseAuthentication(); app.UseAuthorization(); // Добавляем контроллеры и Carter (Minimal APIs) -app.MapControllers(); app.MapCarter(); // Добавляем SignalR хабы diff --git a/backend/src/Host/Application/Admin/Commands/CleanRun/CleanRunCommand.cs b/backend/src/Modules/Admin/Application/Admin/Commands/CleanRun/CleanRunCommand.cs similarity index 89% rename from backend/src/Host/Application/Admin/Commands/CleanRun/CleanRunCommand.cs rename to backend/src/Modules/Admin/Application/Admin/Commands/CleanRun/CleanRunCommand.cs index 0705732..394cb52 100644 --- a/backend/src/Host/Application/Admin/Commands/CleanRun/CleanRunCommand.cs +++ b/backend/src/Modules/Admin/Application/Admin/Commands/CleanRun/CleanRunCommand.cs @@ -1,3 +1,5 @@ +using Knot.Modules.Admin.Application.Admin.DTOs; +using Knot.Modules.Messaging.Domain; using System; using System.Linq; using System.Collections.Generic; @@ -6,15 +8,15 @@ using System.Threading.Tasks; using MediatR; using Knot.Shared.Kernel; using Knot.Shared.Kernel.Storage; -using Knot.Modules.Identity.Infrastructure.Persistence; -using Knot.Modules.Chats.Infrastructure.Persistence; -using Knot.Modules.Chats.Domain; +using Knot.Modules.Auth.Infrastructure.Persistence; +using Knot.Modules.Conversations.Infrastructure.Persistence; +using Knot.Modules.Conversations.Domain; using MongoDB.Driver; using Microsoft.EntityFrameworkCore; namespace Host.Application.Admin.Commands; -public record CleanRunCommand(IFileStorageService FileStorage, IdentityDbContext IdentityDb) : ICommand; +public record CleanRunCommand(IFileStorageService FileStorage, AuthDbContext IdentityDb) : ICommand; internal sealed class CleanRunCommandHandler : ICommandHandler { diff --git a/backend/src/Host/Application/Admin/Commands/ResetUserPassword/ResetUserPasswordCommand.cs b/backend/src/Modules/Admin/Application/Admin/Commands/ResetUserPassword/ResetUserPasswordCommand.cs similarity index 71% rename from backend/src/Host/Application/Admin/Commands/ResetUserPassword/ResetUserPasswordCommand.cs rename to backend/src/Modules/Admin/Application/Admin/Commands/ResetUserPassword/ResetUserPasswordCommand.cs index e6078c8..b4dd5f2 100644 --- a/backend/src/Host/Application/Admin/Commands/ResetUserPassword/ResetUserPasswordCommand.cs +++ b/backend/src/Modules/Admin/Application/Admin/Commands/ResetUserPassword/ResetUserPasswordCommand.cs @@ -1,12 +1,16 @@ +using Knot.Modules.Auth.Domain; +using Knot.Modules.Admin.Application.Admin.DTOs; using System; using System.Threading; using System.Threading.Tasks; using MediatR; using Knot.Shared.Kernel; -using Host.Models; -using Knot.Modules.Identity.Domain; -using Knot.Modules.Identity.Infrastructure.Persistence; -using Knot.Modules.Identity.Application.Abstractions; +using Knot.Modules.Auth.Application.Auth.DTOs; + +using Knot.Modules.Auth.Application.Users; +using Knot.Modules.Auth.Domain; +using Knot.Modules.Auth.Infrastructure.Persistence; +using Knot.Modules.Auth.Application.Abstractions; namespace Host.Application.Admin.Commands; @@ -15,9 +19,9 @@ public record ResetUserPasswordCommand(Guid UserId, string NewPassword) : IComma internal sealed class ResetUserPasswordCommandHandler : ICommandHandler { private readonly IUserRepository _userRepository; - private readonly IIdentityUnitOfWork _identityUnitOfWork; + private readonly IAuthUnitOfWork _identityUnitOfWork; - public ResetUserPasswordCommandHandler(IUserRepository userRepository, IIdentityUnitOfWork identityUnitOfWork) + public ResetUserPasswordCommandHandler(IUserRepository userRepository, IAuthUnitOfWork identityUnitOfWork) { _userRepository = userRepository; _identityUnitOfWork = identityUnitOfWork; @@ -28,7 +32,7 @@ internal sealed class ResetUserPasswordCommandHandler : ICommandHandler(IdentityErrors.UserNotFound); + return Result.Failure(AuthErrors.UserNotFound); } if (string.IsNullOrWhiteSpace(request.NewPassword)) diff --git a/backend/src/Host/Application/Admin/Commands/UpdateSettings/UpdateSettingsCommand.cs b/backend/src/Modules/Admin/Application/Admin/Commands/UpdateSettings/UpdateSettingsCommand.cs similarity index 93% rename from backend/src/Host/Application/Admin/Commands/UpdateSettings/UpdateSettingsCommand.cs rename to backend/src/Modules/Admin/Application/Admin/Commands/UpdateSettings/UpdateSettingsCommand.cs index adf3863..6fe3717 100644 --- a/backend/src/Host/Application/Admin/Commands/UpdateSettings/UpdateSettingsCommand.cs +++ b/backend/src/Modules/Admin/Application/Admin/Commands/UpdateSettings/UpdateSettingsCommand.cs @@ -1,4 +1,5 @@ -using System; +using Knot.Modules.Admin.Application.Admin.DTOs; +using System; using System.Threading; using System.Threading.Tasks; using MediatR; @@ -23,4 +24,4 @@ internal sealed class UpdateSettingsCommandHandler : ICommandHandler; +public record CleanDryRunQuery(IFileStorageService FileStorage, AuthDbContext IdentityDb) : IQuery; internal sealed class CleanDryRunQueryHandler : IQueryHandler { diff --git a/backend/src/Host/Application/Admin/Queries/GetDashboardStats/GetDashboardStatsQuery.cs b/backend/src/Modules/Admin/Application/Admin/Queries/GetDashboardStats/GetDashboardStatsQuery.cs similarity index 91% rename from backend/src/Host/Application/Admin/Queries/GetDashboardStats/GetDashboardStatsQuery.cs rename to backend/src/Modules/Admin/Application/Admin/Queries/GetDashboardStats/GetDashboardStatsQuery.cs index bcb82d2..24d5ec0 100644 --- a/backend/src/Host/Application/Admin/Queries/GetDashboardStats/GetDashboardStatsQuery.cs +++ b/backend/src/Modules/Admin/Application/Admin/Queries/GetDashboardStats/GetDashboardStatsQuery.cs @@ -1,4 +1,5 @@ -using System.Threading; +using Knot.Modules.Admin.Application.Admin.DTOs; +using System.Threading; using System.Threading.Tasks; using MediatR; using Knot.Shared.Kernel; @@ -22,4 +23,4 @@ internal sealed class GetDashboardStatsQueryHandler : IQueryHandler(IdentityErrors.UserNotFound); + return Result.Failure(AuthErrors.UserNotFound); } var filter = Builders.Filter.Eq(m => m.SenderId, request.UserId); @@ -56,8 +61,8 @@ internal sealed class GetUserDetailsQueryHandler : IQueryHandler + + net10.0 + enable + enable + + + + + + + + + + + + + + <_Parameter1>Knot.Modules.Admin.UnitTests + + + \ No newline at end of file diff --git a/backend/src/Modules/Admin/Presentation/Endpoints/AdminEndpoints.cs b/backend/src/Modules/Admin/Presentation/Endpoints/AdminEndpoints.cs new file mode 100644 index 0000000..c7c0848 --- /dev/null +++ b/backend/src/Modules/Admin/Presentation/Endpoints/AdminEndpoints.cs @@ -0,0 +1,86 @@ +using Carter; +using Knot.Modules.Auth.Application.Auth.DTOs; +using Knot.Shared.Kernel; +using Knot.Shared.Kernel.Configuration; +using Knot.Modules.Auth.Application.Users.Register; +using Knot.Shared.Kernel.Storage; +using Knot.Modules.Auth.Infrastructure.Persistence; +using MediatR; +using Host.Application.Admin.Queries; +using Host.Application.Admin.Commands; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Mvc; + +namespace Knot.Host.Presentation.Endpoints; + +public sealed class AdminEndpoints : ICarterModule +{ + public void AddRoutes(IEndpointRouteBuilder app) + { + var group = app.MapGroup("api/admin"); // Middleware handles auth + + group.MapGet("settings", async (ISender sender, CancellationToken ct) => + { + var result = await sender.Send(new GetSettingsQuery(), ct); + return Results.Ok(result.Value); + }); + + group.MapPut("settings", async ([FromBody] SystemSettingsDto settings, ISender sender, CancellationToken ct) => + { + var result = await sender.Send(new UpdateSettingsCommand(settings), ct); + return Results.Ok(result.Value); + }); + + group.MapGet("dashboard", async (ISender sender, CancellationToken ct) => + { + var result = await sender.Send(new GetDashboardStatsQuery(), ct); + return Results.Ok(result.Value); + }); + + group.MapPost("users", async ([FromBody] RegisterUserCommand command, ISender sender, CancellationToken ct) => + { + var result = await sender.Send(command, ct); + if (result.IsFailure) return Results.BadRequest(new { error = result.Error.Description }); + + var userDetails = await sender.Send(new GetUserDetailsQuery(result.Value.User.Id), ct); + return Results.Ok(userDetails.Value); + }); + + group.MapPost("users/{userId:guid}/reset-password", async (Guid userId, [FromBody] ResetPasswordDto dto, ISender sender, CancellationToken ct) => + { + var result = await sender.Send(new ResetUserPasswordCommand(userId, dto.NewPassword), ct); + if (result.IsFailure) + { + if (result.Error.Code == "User.NotFound") return Results.NotFound(new { error = "User not found" }); + return Results.BadRequest(new { error = result.Error.Description }); + } + return Results.Ok(result.Value); + }); + + group.MapGet("users", async (ISender sender, [FromQuery] string query = "", CancellationToken ct = default) => + { + var result = await sender.Send(new SearchUsersQuery(query), ct); + return Results.Ok(result.Value); + }); + + group.MapGet("users/{id:guid}", async (Guid id, ISender sender, CancellationToken ct) => + { + var result = await sender.Send(new GetUserDetailsQuery(id), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound("User not found"); + }); + + group.MapGet("clean/dry-run", async (IFileStorageService fileStorage, AuthDbContext identityDb, ISender sender, CancellationToken ct) => + { + var result = await sender.Send(new CleanDryRunQuery(fileStorage, identityDb), ct); + return Results.Ok(result.Value); + }); + + group.MapPost("clean/run", async (IFileStorageService fileStorage, AuthDbContext identityDb, ISender sender, CancellationToken ct) => + { + var result = await sender.Send(new CleanRunCommand(fileStorage, identityDb), ct); + return Results.Ok(result.Value); + }); + } +} \ No newline at end of file diff --git a/backend/src/Modules/Identity/Application/Abstractions/IIdentityDbContext.cs b/backend/src/Modules/Auth/Application/Abstractions/IIdentityDbContext.cs similarity index 59% rename from backend/src/Modules/Identity/Application/Abstractions/IIdentityDbContext.cs rename to backend/src/Modules/Auth/Application/Abstractions/IIdentityDbContext.cs index c02d5e2..a323aa8 100644 --- a/backend/src/Modules/Identity/Application/Abstractions/IIdentityDbContext.cs +++ b/backend/src/Modules/Auth/Application/Abstractions/IIdentityDbContext.cs @@ -1,14 +1,14 @@ using System.Threading; using System.Threading.Tasks; -using Knot.Modules.Identity.Domain; +using Knot.Modules.Auth.Domain; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -namespace Knot.Modules.Identity.Application.Abstractions; +namespace Knot.Modules.Auth.Application.Abstractions; -public interface IIdentityDbContext +public interface IAuthDbContext { DbSet Users { get; } - DbSet Friendships { get; } Task SaveChangesAsync(CancellationToken cancellationToken); } + diff --git a/backend/src/Modules/Identity/Application/Abstractions/IIdentityUnitOfWork.cs b/backend/src/Modules/Auth/Application/Abstractions/IIdentityUnitOfWork.cs similarity index 54% rename from backend/src/Modules/Identity/Application/Abstractions/IIdentityUnitOfWork.cs rename to backend/src/Modules/Auth/Application/Abstractions/IIdentityUnitOfWork.cs index ca4361c..11f260e 100644 --- a/backend/src/Modules/Identity/Application/Abstractions/IIdentityUnitOfWork.cs +++ b/backend/src/Modules/Auth/Application/Abstractions/IIdentityUnitOfWork.cs @@ -1,10 +1,11 @@ using Knot.Shared.Kernel; -namespace Knot.Modules.Identity.Application.Abstractions; +namespace Knot.Modules.Auth.Application.Abstractions; /// /// Unit of Work специфичный для модуля Identity. /// -public interface IIdentityUnitOfWork : IUnitOfWork +public interface IAuthUnitOfWork : IUnitOfWork { } + diff --git a/backend/src/Modules/Auth/Application/Abstractions/IJwtTokenProvider.cs b/backend/src/Modules/Auth/Application/Abstractions/IJwtTokenProvider.cs new file mode 100644 index 0000000..4cf048e --- /dev/null +++ b/backend/src/Modules/Auth/Application/Abstractions/IJwtTokenProvider.cs @@ -0,0 +1,9 @@ +using Knot.Modules.Auth.Domain; + +namespace Knot.Modules.Auth.Application.Abstractions; + +public interface IJwtTokenProvider +{ + string Generate(User user); +} + diff --git a/backend/src/Host/Models/Auth/AuthResponseDto.cs b/backend/src/Modules/Auth/Application/Auth/DTOs/AuthResponseDto.cs similarity index 85% rename from backend/src/Host/Models/Auth/AuthResponseDto.cs rename to backend/src/Modules/Auth/Application/Auth/DTOs/AuthResponseDto.cs index 31a503a..4e28fd4 100644 --- a/backend/src/Host/Models/Auth/AuthResponseDto.cs +++ b/backend/src/Modules/Auth/Application/Auth/DTOs/AuthResponseDto.cs @@ -1,6 +1,6 @@ using System; -namespace Host.Models; +namespace Knot.Modules.Auth.Application.Auth.DTOs; public record AuthResponseDto( string Token, @@ -18,3 +18,4 @@ public record AuthUserDto( bool IsOnline, DateTime CreatedAt ); + diff --git a/backend/src/Host/Models/Auth/ResetPasswordDto.cs b/backend/src/Modules/Auth/Application/Auth/DTOs/ResetPasswordDto.cs similarity index 64% rename from backend/src/Host/Models/Auth/ResetPasswordDto.cs rename to backend/src/Modules/Auth/Application/Auth/DTOs/ResetPasswordDto.cs index feeb162..d2a1d2a 100644 --- a/backend/src/Host/Models/Auth/ResetPasswordDto.cs +++ b/backend/src/Modules/Auth/Application/Auth/DTOs/ResetPasswordDto.cs @@ -1,6 +1,7 @@ -namespace Host.Models; +namespace Knot.Modules.Auth.Application.Auth.DTOs; public class ResetPasswordDto { public string NewPassword { get; set; } = string.Empty; } + diff --git a/backend/src/Modules/Identity/Application/Users/Auth/AuthResponseDto.cs b/backend/src/Modules/Auth/Application/Users/Auth/AuthResponseDto.cs similarity index 84% rename from backend/src/Modules/Identity/Application/Users/Auth/AuthResponseDto.cs rename to backend/src/Modules/Auth/Application/Users/Auth/AuthResponseDto.cs index fdedad6..17f2752 100644 --- a/backend/src/Modules/Identity/Application/Users/Auth/AuthResponseDto.cs +++ b/backend/src/Modules/Auth/Application/Users/Auth/AuthResponseDto.cs @@ -1,6 +1,6 @@ using System; -namespace Knot.Modules.Identity.Application.Users.Auth; +namespace Knot.Modules.Auth.Application.Users.Auth; public record AuthResponseDto( string Token, @@ -18,3 +18,4 @@ public record AuthUserDto( bool IsOnline, DateTime CreatedAt ); + diff --git a/backend/src/Modules/Identity/Application/Users/GetMe/GetMe.cs b/backend/src/Modules/Auth/Application/Users/GetMe/GetMe.cs similarity index 78% rename from backend/src/Modules/Identity/Application/Users/GetMe/GetMe.cs rename to backend/src/Modules/Auth/Application/Users/GetMe/GetMe.cs index 2db2b19..7384a81 100644 --- a/backend/src/Modules/Identity/Application/Users/GetMe/GetMe.cs +++ b/backend/src/Modules/Auth/Application/Users/GetMe/GetMe.cs @@ -1,9 +1,9 @@ using Knot.Shared.Kernel; -using Knot.Modules.Identity.Domain; -using Knot.Modules.Identity.Application.Abstractions; -using Knot.Modules.Identity.Application.Users.Auth; +using Knot.Modules.Auth.Domain; -namespace Knot.Modules.Identity.Application.Users.GetMe; +using Knot.Modules.Auth.Application.Users.Auth; + +namespace Knot.Modules.Auth.Application.Users.GetMe; public sealed record GetMeQuery(Guid UserId) : IQuery; @@ -21,7 +21,7 @@ internal sealed class GetMeQueryHandler : IQueryHandler(IdentityErrors.UserNotFound); + return Result.Failure(AuthErrors.UserNotFound); } var response = new AuthResponseDto( @@ -42,3 +42,4 @@ internal sealed class GetMeQueryHandler : IQueryHandler /// Команда для входа пользователя. Возвращает AuthResponseDto. @@ -28,7 +29,7 @@ public sealed class LoginUserCommandHandler : ICommandHandler(IdentityErrors.IdentityInvalidCredentials); + return Result.Failure(AuthErrors.IdentityInvalidCredentials); } string token = _tokenProvider.Generate(user); @@ -49,3 +50,4 @@ public sealed class LoginUserCommandHandler : ICommandHandler /// Команда для регистрации нового пользователя. @@ -23,13 +24,13 @@ public sealed record RegisterUserCommand( public sealed class RegisterUserCommandHandler : ICommandHandler { private readonly IUserRepository _userRepository; - private readonly IIdentityUnitOfWork _unitOfWork; + private readonly IAuthUnitOfWork _unitOfWork; private readonly ISettingsService _settings; private readonly IJwtTokenProvider _tokenProvider; public RegisterUserCommandHandler( IUserRepository userRepository, - IIdentityUnitOfWork unitOfWork, + IAuthUnitOfWork unitOfWork, ISettingsService settings, IJwtTokenProvider tokenProvider) { @@ -43,13 +44,13 @@ public sealed class RegisterUserCommandHandler : ICommandHandler(IdentityErrors.IdentityRegistrationDisabled); + return Result.Failure(AuthErrors.IdentityRegistrationDisabled); } // 1. Проверка уникальности username if (!await _userRepository.IsUsernameUniqueAsync(request.Username, cancellationToken)) { - return Result.Failure(IdentityErrors.IdentityUsernameNotUnique); + return Result.Failure(AuthErrors.IdentityUsernameNotUnique); } // 2. Хеширование пароля (здесь будет вызов сервиса, пока заглушка) @@ -85,3 +86,4 @@ public sealed class RegisterUserCommandHandler : ICommandHandler /// Регистрация сервисов модуля Identity. /// - public static IServiceCollection AddIdentityModule( + public static IServiceCollection AddAuthModule( this IServiceCollection services, IConfiguration configuration) { // Настройка базы данных string connectionString = configuration.GetConnectionString("DefaultConnection")!; - services.AddDbContext(options => + services.AddDbContext(options => options.UseNpgsql(connectionString)); // Регистрация Unit of Work и Репозиториев - services.AddScoped(sp => sp.GetRequiredService()); - services.AddScoped(sp => sp.GetRequiredService()); + services.AddScoped(sp => sp.GetRequiredService()); + services.AddScoped(sp => sp.GetRequiredService()); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); // Регистрация MediatR для этого модуля services.AddMediatR(config => @@ -38,3 +38,4 @@ public static class DependencyInjection return services; } } + diff --git a/backend/src/Modules/Identity/Domain/IdentityErrors.cs b/backend/src/Modules/Auth/Domain/AuthErrors.cs similarity index 92% rename from backend/src/Modules/Identity/Domain/IdentityErrors.cs rename to backend/src/Modules/Auth/Domain/AuthErrors.cs index 5e317da..8361373 100644 --- a/backend/src/Modules/Identity/Domain/IdentityErrors.cs +++ b/backend/src/Modules/Auth/Domain/AuthErrors.cs @@ -1,8 +1,8 @@ using Knot.Shared.Kernel; -namespace Knot.Modules.Identity.Domain; +namespace Knot.Modules.Auth.Domain; -public static class IdentityErrors +public static class AuthErrors { public static readonly Error FriendsNotFound = new Error("Friends.NotFound", "Friendship not found"); public static readonly Error FriendsSelf = new Error("Friends.Self", "Cannot add yourself"); @@ -12,3 +12,4 @@ public static class IdentityErrors public static readonly Error IdentityRegistrationDisabled = new Error("Identity.RegistrationDisabled", "Registration is disabled by the administrator."); public static readonly Error IdentityUsernameNotUnique = new Error("Identity.UsernameNotUnique", "Это имя пользователя уже занято."); } + diff --git a/backend/src/Modules/Identity/Domain/IUserRepository.cs b/backend/src/Modules/Auth/Domain/IUserRepository.cs similarity index 90% rename from backend/src/Modules/Identity/Domain/IUserRepository.cs rename to backend/src/Modules/Auth/Domain/IUserRepository.cs index cdf77d6..287e2cc 100644 --- a/backend/src/Modules/Identity/Domain/IUserRepository.cs +++ b/backend/src/Modules/Auth/Domain/IUserRepository.cs @@ -1,6 +1,6 @@ -using Knot.Modules.Identity.Domain; +using Knot.Modules.Auth.Domain; -namespace Knot.Modules.Identity.Domain; +namespace Knot.Modules.Auth.Domain; /// /// Интерфейс репозитория для работы с пользователями. @@ -15,3 +15,4 @@ public interface IUserRepository void Add(User user); void Update(User user); } + diff --git a/backend/src/Modules/Identity/Domain/User.cs b/backend/src/Modules/Auth/Domain/User.cs similarity index 97% rename from backend/src/Modules/Identity/Domain/User.cs rename to backend/src/Modules/Auth/Domain/User.cs index 1744493..0820d44 100644 --- a/backend/src/Modules/Identity/Domain/User.cs +++ b/backend/src/Modules/Auth/Domain/User.cs @@ -1,6 +1,6 @@ using Knot.Shared.Kernel; -namespace Knot.Modules.Identity.Domain; +namespace Knot.Modules.Auth.Domain; /// /// Сущность пользователя в контексте идентификации (Identity). @@ -58,3 +58,4 @@ public sealed class User : AggregateRoot PasswordHash = newPasswordHash; } } + diff --git a/backend/src/Modules/Identity/Infrastructure/Authentication/JwtTokenProvider.cs b/backend/src/Modules/Auth/Infrastructure/Authentication/JwtTokenProvider.cs similarity index 89% rename from backend/src/Modules/Identity/Infrastructure/Authentication/JwtTokenProvider.cs rename to backend/src/Modules/Auth/Infrastructure/Authentication/JwtTokenProvider.cs index 7ce5b4f..35037eb 100644 --- a/backend/src/Modules/Identity/Infrastructure/Authentication/JwtTokenProvider.cs +++ b/backend/src/Modules/Auth/Infrastructure/Authentication/JwtTokenProvider.cs @@ -3,10 +3,10 @@ using System.Security.Claims; using System.Text; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; -using Knot.Modules.Identity.Application.Abstractions; -using Knot.Modules.Identity.Domain; +using Knot.Modules.Auth.Application.Abstractions; +using Knot.Modules.Auth.Domain; -namespace Knot.Modules.Identity.Infrastructure.Authentication; +namespace Knot.Modules.Auth.Infrastructure.Authentication; public sealed class JwtTokenProvider : IJwtTokenProvider { @@ -43,3 +43,4 @@ public sealed class JwtTokenProvider : IJwtTokenProvider return new JwtSecurityTokenHandler().WriteToken(token); } } + diff --git a/backend/src/Modules/Auth/Infrastructure/Persistence/AuthDbContext.cs b/backend/src/Modules/Auth/Infrastructure/Persistence/AuthDbContext.cs new file mode 100644 index 0000000..ce03a31 --- /dev/null +++ b/backend/src/Modules/Auth/Infrastructure/Persistence/AuthDbContext.cs @@ -0,0 +1,81 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Metadata; +using Knot.Modules.Auth.Application.Abstractions; +using Knot.Modules.Auth.Domain; +using Knot.Shared.Kernel; +using Knot.Shared.Kernel.Security; + +namespace Knot.Modules.Auth.Infrastructure.Persistence; + +/// +/// Контекст базы данных для модуля Identity. +/// +public sealed class AuthDbContext : DbContext, IAuthUnitOfWork, Knot.Modules.Auth.Application.Abstractions.IAuthDbContext +{ + private readonly IMediator _mediator; + private readonly IEncryptionService _encryptionService; + + public AuthDbContext(DbContextOptions options, IMediator mediator, IEncryptionService encryptionService) + : base(options) + { + _mediator = mediator; + _encryptionService = encryptionService; + } + + public DbSet Users => Set(); + + + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning)); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema("identity"); + + + + modelBuilder.Entity(builder => + { + builder.ToTable("Users"); + builder.HasKey(u => u.Id); + builder.Property(u => u.Username).IsRequired().HasMaxLength(50); + builder.HasIndex(u => u.Username).IsUnique(); + builder.Property(u => u.PasswordHash).IsRequired(); + + }); + } + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + var domainEvents = ChangeTracker + .Entries() + .SelectMany(x => + + { + if (x.Entity is AggregateRoot root) + { + var events = root.GetDomainEvents().ToList(); + root.ClearDomainEvents(); + return events; + } + return Enumerable.Empty(); + }) + .ToList(); + + int result = await base.SaveChangesAsync(cancellationToken); + + foreach (var domainEvent in domainEvents) + { + await _mediator.Publish(domainEvent, cancellationToken); + } + + return result; + } +} + diff --git a/backend/src/Modules/Identity/Infrastructure/Persistence/UserRepository.cs b/backend/src/Modules/Auth/Infrastructure/Persistence/UserRepository.cs similarity index 89% rename from backend/src/Modules/Identity/Infrastructure/Persistence/UserRepository.cs rename to backend/src/Modules/Auth/Infrastructure/Persistence/UserRepository.cs index c81efc9..91835b8 100644 --- a/backend/src/Modules/Identity/Infrastructure/Persistence/UserRepository.cs +++ b/backend/src/Modules/Auth/Infrastructure/Persistence/UserRepository.cs @@ -1,16 +1,16 @@ using Microsoft.EntityFrameworkCore; -using Knot.Modules.Identity.Domain; +using Knot.Modules.Auth.Domain; -namespace Knot.Modules.Identity.Infrastructure.Persistence; +namespace Knot.Modules.Auth.Infrastructure.Persistence; /// /// Реализация репозитория пользователей с использованием EF Core. /// public sealed class UserRepository : IUserRepository { - private readonly IdentityDbContext _context; + private readonly AuthDbContext _context; - public UserRepository(IdentityDbContext context) + public UserRepository(AuthDbContext context) { _context = context; } @@ -54,3 +54,4 @@ public sealed class UserRepository : IUserRepository _context.Users.Update(user); } } + diff --git a/backend/src/Modules/Identity/Infrastructure/Services/UserDisplayNameProvider.cs b/backend/src/Modules/Auth/Infrastructure/Services/UserDisplayNameProvider.cs similarity index 93% rename from backend/src/Modules/Identity/Infrastructure/Services/UserDisplayNameProvider.cs rename to backend/src/Modules/Auth/Infrastructure/Services/UserDisplayNameProvider.cs index 04b9af8..94b4822 100644 --- a/backend/src/Modules/Identity/Infrastructure/Services/UserDisplayNameProvider.cs +++ b/backend/src/Modules/Auth/Infrastructure/Services/UserDisplayNameProvider.cs @@ -1,7 +1,7 @@ using Knot.Shared.Kernel; -using Knot.Modules.Identity.Domain; +using Knot.Modules.Auth.Domain; -namespace Knot.Modules.Identity.Infrastructure.Services; +namespace Knot.Modules.Auth.Infrastructure.Services; public sealed class UserDisplayNameProvider : IUserDisplayNameProvider { @@ -40,3 +40,4 @@ public sealed class UserDisplayNameProvider : IUserDisplayNameProvider return result; } } + diff --git a/backend/src/Modules/Identity/Knot.Modules.Identity.csproj b/backend/src/Modules/Auth/Knot.Modules.Auth.csproj similarity index 91% rename from backend/src/Modules/Identity/Knot.Modules.Identity.csproj rename to backend/src/Modules/Auth/Knot.Modules.Auth.csproj index f2abb95..887161b 100644 --- a/backend/src/Modules/Identity/Knot.Modules.Identity.csproj +++ b/backend/src/Modules/Auth/Knot.Modules.Auth.csproj @@ -8,10 +8,12 @@ + + diff --git a/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311180816_InitialIdentity.Designer.cs b/backend/src/Modules/Auth/Migrations/20260322191927_InitialAuth.Designer.cs similarity index 62% rename from backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311180816_InitialIdentity.Designer.cs rename to backend/src/Modules/Auth/Migrations/20260322191927_InitialAuth.Designer.cs index ac05526..0c967f8 100644 --- a/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311180816_InitialIdentity.Designer.cs +++ b/backend/src/Modules/Auth/Migrations/20260322191927_InitialAuth.Designer.cs @@ -1,19 +1,19 @@ -// +// using System; +using Knot.Modules.Auth.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Knot.Modules.Identity.Infrastructure.Persistence; #nullable disable -namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations +namespace Knot.Modules.Auth.Migrations { - [DbContext(typeof(IdentityDbContext))] - [Migration("20260311180816_InitialIdentity")] - partial class InitialIdentity + [DbContext(typeof(AuthDbContext))] + [Migration("20260322191927_InitialAuth")] + partial class InitialAuth { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -26,20 +26,33 @@ namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("Knot.Modules.Identity.Domain.User", b => + modelBuilder.Entity("Knot.Modules.Auth.Domain.User", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("Bio") + .HasColumnType("text"); + + b.Property("Birthday") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + b.Property("DisplayName") .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); + .HasColumnType("text"); b.Property("Email") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); + .HasColumnType("text"); + + b.Property("HideStoryViews") + .HasColumnType("boolean"); b.Property("PasswordHash") .IsRequired() diff --git a/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311180816_InitialIdentity.cs b/backend/src/Modules/Auth/Migrations/20260322191927_InitialAuth.cs similarity index 64% rename from backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311180816_InitialIdentity.cs rename to backend/src/Modules/Auth/Migrations/20260322191927_InitialAuth.cs index 7086701..2f03e9a 100644 --- a/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311180816_InitialIdentity.cs +++ b/backend/src/Modules/Auth/Migrations/20260322191927_InitialAuth.cs @@ -1,12 +1,12 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations +namespace Knot.Modules.Auth.Migrations { /// - public partial class InitialIdentity : Migration + public partial class InitialAuth : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -22,8 +22,13 @@ namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations Id = table.Column(type: "uuid", nullable: false), Username = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), PasswordHash = table.Column(type: "text", nullable: false), - DisplayName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - Email = table.Column(type: "character varying(255)", maxLength: 255, nullable: true) + DisplayName = table.Column(type: "text", nullable: false), + Email = table.Column(type: "text", nullable: true), + Bio = table.Column(type: "text", nullable: true), + Avatar = table.Column(type: "text", nullable: true), + Birthday = table.Column(type: "timestamp with time zone", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + HideStoryViews = table.Column(type: "boolean", nullable: false) }, constraints: table => { diff --git a/backend/src/Modules/Auth/Migrations/AuthDbContextModelSnapshot.cs b/backend/src/Modules/Auth/Migrations/AuthDbContextModelSnapshot.cs new file mode 100644 index 0000000..6bdbe0f --- /dev/null +++ b/backend/src/Modules/Auth/Migrations/AuthDbContextModelSnapshot.cs @@ -0,0 +1,73 @@ +// +using System; +using Knot.Modules.Auth.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Knot.Modules.Auth.Migrations +{ + [DbContext(typeof(AuthDbContext))] + partial class AuthDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("identity") + .HasAnnotation("ProductVersion", "10.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Knot.Modules.Auth.Domain.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("Bio") + .HasColumnType("text"); + + b.Property("Birthday") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("HideStoryViews") + .HasColumnType("boolean"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users", "identity"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Modules/Auth/Presentation/Endpoints/AuthEndpoints.cs b/backend/src/Modules/Auth/Presentation/Endpoints/AuthEndpoints.cs new file mode 100644 index 0000000..d522a27 --- /dev/null +++ b/backend/src/Modules/Auth/Presentation/Endpoints/AuthEndpoints.cs @@ -0,0 +1,39 @@ +using Carter; +using Knot.Shared.Kernel; +using Knot.Modules.Auth.Application.Users.Login; +using Knot.Modules.Auth.Application.Users.Register; +using Knot.Modules.Auth.Application.Users.GetMe; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Mvc; + +namespace Knot.Modules.Auth.Presentation.Endpoints; + +public sealed class AuthEndpoints : ICarterModule +{ + public void AddRoutes(IEndpointRouteBuilder app) + { + var group = app.MapGroup("api/auth"); + + group.MapPost("register", async ([FromBody] RegisterUserCommand command, ISender sender, CancellationToken ct) => + { + var result = await sender.Send(command, ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(new { error = result.Error.Code ?? result.Error.Description }); + }); + + group.MapPost("login", async ([FromBody] LoginUserCommand command, ISender sender, CancellationToken ct) => + { + var result = await sender.Send(command, ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.Unauthorized(); + }); + + group.MapGet("me", async (ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new GetMeQuery(userContext.UserId), ct); + return result.IsSuccess ? Results.Ok(new { User = result.Value.User }) : Results.NotFound(); + }).RequireAuthorization(); + } +} + diff --git a/backend/src/Modules/Identity/migrations.sql b/backend/src/Modules/Auth/migrations.sql similarity index 100% rename from backend/src/Modules/Identity/migrations.sql rename to backend/src/Modules/Auth/migrations.sql diff --git a/backend/src/Modules/Chats/Application/DTOs/CreatePersonalChatRequest.cs b/backend/src/Modules/Chats/Application/DTOs/CreatePersonalChatRequest.cs deleted file mode 100644 index 8683e77..0000000 --- a/backend/src/Modules/Chats/Application/DTOs/CreatePersonalChatRequest.cs +++ /dev/null @@ -1,5 +0,0 @@ -using System; - -namespace Knot.Modules.Chats.Application.DTOs; - -public sealed record CreatePersonalChatRequest(Guid UserId); diff --git a/backend/src/Modules/Chats/Application/DTOs/TogglePinResponse.cs b/backend/src/Modules/Chats/Application/DTOs/TogglePinResponse.cs deleted file mode 100644 index 57c2ce0..0000000 --- a/backend/src/Modules/Chats/Application/DTOs/TogglePinResponse.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Knot.Modules.Chats.Application.DTOs; - -public record TogglePinResponse(bool IsPinned); diff --git a/backend/src/Modules/Chats/Migrations/20260319124845_InitialChats.Designer.cs b/backend/src/Modules/Chats/Migrations/20260319124845_InitialChats.Designer.cs deleted file mode 100644 index 4aeba6c..0000000 --- a/backend/src/Modules/Chats/Migrations/20260319124845_InitialChats.Designer.cs +++ /dev/null @@ -1,99 +0,0 @@ -// -using System; -using Knot.Modules.Chats.Infrastructure.Persistence; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Knot.Modules.Chats.Migrations -{ - [DbContext(typeof(ChatsDbContext))] - [Migration("20260319124845_InitialChats")] - partial class InitialChats - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasDefaultSchema("chats") - .HasAnnotation("ProductVersion", "10.0.4") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Avatar") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Description") - .HasColumnType("text"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Chats", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.ChatMember", "Members", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("ChatId") - .HasColumnType("uuid"); - - b1.Property("IsMuted") - .HasColumnType("boolean"); - - b1.Property("IsPinned") - .HasColumnType("boolean"); - - b1.Property("JoinedAt") - .HasColumnType("timestamp with time zone"); - - b1.Property("Role") - .IsRequired() - .HasColumnType("text"); - - b1.Property("UserId") - .HasColumnType("uuid"); - - b1.HasKey("Id"); - - b1.HasIndex("ChatId", "UserId") - .IsUnique(); - - b1.ToTable("ChatMembers", "chats"); - - b1.WithOwner() - .HasForeignKey("ChatId"); - }); - - b.Navigation("Members"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/backend/src/Modules/Chats/Migrations/20260320185319_AddHighWaterMark.cs b/backend/src/Modules/Chats/Migrations/20260320185319_AddHighWaterMark.cs deleted file mode 100644 index a9da1fc..0000000 --- a/backend/src/Modules/Chats/Migrations/20260320185319_AddHighWaterMark.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Knot.Modules.Chats.Migrations -{ - /// - public partial class AddHighWaterMark : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "LastMessageSequenceId", - schema: "chats", - table: "Chats", - type: "bigint", - nullable: false, - defaultValue: 0L); - - migrationBuilder.AddColumn( - name: "LastDeliveredMessageId", - schema: "chats", - table: "ChatMembers", - type: "uuid", - nullable: true); - - migrationBuilder.AddColumn( - name: "LastReadMessageId", - schema: "chats", - table: "ChatMembers", - type: "uuid", - nullable: true); - - migrationBuilder.AddColumn( - name: "LastReadSequenceId", - schema: "chats", - table: "ChatMembers", - type: "bigint", - nullable: false, - defaultValue: 0L); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "LastMessageSequenceId", - schema: "chats", - table: "Chats"); - - migrationBuilder.DropColumn( - name: "LastDeliveredMessageId", - schema: "chats", - table: "ChatMembers"); - - migrationBuilder.DropColumn( - name: "LastReadMessageId", - schema: "chats", - table: "ChatMembers"); - - migrationBuilder.DropColumn( - name: "LastReadSequenceId", - schema: "chats", - table: "ChatMembers"); - } - } -} diff --git a/backend/src/Modules/Chats/Application/Abstractions/IChatsUnitOfWork.cs b/backend/src/Modules/Conversations/Application/Abstractions/IChatsUnitOfWork.cs similarity index 73% rename from backend/src/Modules/Chats/Application/Abstractions/IChatsUnitOfWork.cs rename to backend/src/Modules/Conversations/Application/Abstractions/IChatsUnitOfWork.cs index 5a52644..6c9bdb2 100644 --- a/backend/src/Modules/Chats/Application/Abstractions/IChatsUnitOfWork.cs +++ b/backend/src/Modules/Conversations/Application/Abstractions/IChatsUnitOfWork.cs @@ -1,6 +1,6 @@ using Knot.Shared.Kernel; -namespace Knot.Modules.Chats.Application.Abstractions; +namespace Knot.Modules.Conversations.Application.Abstractions; /// /// Unit of Work специфичный для модуля Chats. @@ -8,3 +8,4 @@ namespace Knot.Modules.Chats.Application.Abstractions; public interface IChatsUnitOfWork : IUnitOfWork { } + diff --git a/backend/src/Modules/Chats/Application/Chats/Avatar/Avatar.cs b/backend/src/Modules/Conversations/Application/Chats/Avatar/Avatar.cs similarity index 96% rename from backend/src/Modules/Chats/Application/Chats/Avatar/Avatar.cs rename to backend/src/Modules/Conversations/Application/Chats/Avatar/Avatar.cs index 52168d0..64189be 100644 --- a/backend/src/Modules/Chats/Application/Chats/Avatar/Avatar.cs +++ b/backend/src/Modules/Conversations/Application/Chats/Avatar/Avatar.cs @@ -4,14 +4,14 @@ using System.Threading; using System.Threading.Tasks; using Knot.Shared.Kernel; using Knot.Shared.Kernel.Storage; -using Knot.Modules.Chats.Domain; -using Knot.Modules.Chats.Application.Abstractions; +using Knot.Modules.Conversations.Domain; +using Knot.Modules.Conversations.Application.Abstractions; using MediatR; using System.Linq; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; -namespace Knot.Modules.Chats.Application.Chats.Avatar; +namespace Knot.Modules.Conversations.Application.Chats.Avatar; public record UploadGroupAvatarCommand(Guid ChatId, Guid UserId, string FileName, string ContentType, Stream FileStream) : ICommand; @@ -127,3 +127,4 @@ internal sealed class RemoveGroupAvatarCommandHandler : ICommandHandler; @@ -32,3 +32,4 @@ internal sealed class ClearChatCommandHandler : ICommandHandler /// Команда для создания чата. @@ -41,3 +41,4 @@ public sealed class CreateChatCommandHandler : ICommandHandler; @@ -153,3 +155,5 @@ internal sealed class GetChatByIdQueryHandler : IQueryHandler>; @@ -147,3 +149,5 @@ internal sealed class GetChatsQueryHandler : IQueryHandler; @@ -36,3 +36,4 @@ public sealed class GetOrCreateFavoritesCommandHandler : ICommandHandler; @@ -51,3 +51,4 @@ internal sealed class LeaveOrDeleteChatCommandHandler : ICommandHandler UserIdsToAdd) : ICommand; @@ -70,3 +70,4 @@ internal sealed class RemoveMemberCommandHandler : ICommandHandler; @@ -44,3 +44,4 @@ internal sealed class TogglePinCommandHandler : ICommandHandler; @@ -46,3 +46,4 @@ internal sealed class UpdateChatCommandHandler : ICommandHandler UserIds); + diff --git a/backend/src/Modules/Chats/Application/DTOs/ChatDto.cs b/backend/src/Modules/Conversations/Application/DTOs/ChatDto.cs similarity index 83% rename from backend/src/Modules/Chats/Application/DTOs/ChatDto.cs rename to backend/src/Modules/Conversations/Application/DTOs/ChatDto.cs index 8664e61..de4a3c6 100644 --- a/backend/src/Modules/Chats/Application/DTOs/ChatDto.cs +++ b/backend/src/Modules/Conversations/Application/DTOs/ChatDto.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace Knot.Modules.Chats.Application.DTOs; +namespace Knot.Modules.Conversations.Application.DTOs; public record ChatDto( Guid Id, @@ -14,3 +14,4 @@ public record ChatDto( List Messages, int UnreadCount ); + diff --git a/backend/src/Modules/Chats/Application/DTOs/ChatMemberDto.cs b/backend/src/Modules/Conversations/Application/DTOs/ChatMemberDto.cs similarity index 58% rename from backend/src/Modules/Chats/Application/DTOs/ChatMemberDto.cs rename to backend/src/Modules/Conversations/Application/DTOs/ChatMemberDto.cs index 7798a15..70a9024 100644 --- a/backend/src/Modules/Chats/Application/DTOs/ChatMemberDto.cs +++ b/backend/src/Modules/Conversations/Application/DTOs/ChatMemberDto.cs @@ -1,7 +1,7 @@ -using Knot.Modules.Chats.Domain; +using Knot.Modules.Conversations.Domain; using System; -namespace Knot.Modules.Chats.Application.DTOs; +namespace Knot.Modules.Conversations.Application.DTOs; public record ChatMemberDto( Guid Id, @@ -10,3 +10,4 @@ public record ChatMemberDto( bool IsPinned, ChatUserDto? User ); + diff --git a/backend/src/Modules/Chats/Application/DTOs/ChatMessageDto.cs b/backend/src/Modules/Conversations/Application/DTOs/ChatMessageDto.cs similarity index 89% rename from backend/src/Modules/Chats/Application/DTOs/ChatMessageDto.cs rename to backend/src/Modules/Conversations/Application/DTOs/ChatMessageDto.cs index 2589fc5..1e16e6e 100644 --- a/backend/src/Modules/Chats/Application/DTOs/ChatMessageDto.cs +++ b/backend/src/Modules/Conversations/Application/DTOs/ChatMessageDto.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace Knot.Modules.Chats.Application.DTOs; +namespace Knot.Modules.Conversations.Application.DTOs; public record ChatMessageDto( Guid Id, @@ -23,3 +23,4 @@ public record ChatMessageDto( List Reactions, List ReadBy ); + diff --git a/backend/src/Modules/Chats/Application/DTOs/ChatUserDto.cs b/backend/src/Modules/Conversations/Application/DTOs/ChatUserDto.cs similarity index 74% rename from backend/src/Modules/Chats/Application/DTOs/ChatUserDto.cs rename to backend/src/Modules/Conversations/Application/DTOs/ChatUserDto.cs index 22cd440..cba1c13 100644 --- a/backend/src/Modules/Chats/Application/DTOs/ChatUserDto.cs +++ b/backend/src/Modules/Conversations/Application/DTOs/ChatUserDto.cs @@ -1,6 +1,6 @@ using System; -namespace Knot.Modules.Chats.Application.DTOs; +namespace Knot.Modules.Conversations.Application.DTOs; public record ChatUserDto( Guid Id, @@ -10,3 +10,4 @@ public record ChatUserDto( bool IsOnline, DateTime LastSeen ); + diff --git a/backend/src/Modules/Chats/Application/DTOs/CreateChatRequest.cs b/backend/src/Modules/Conversations/Application/DTOs/CreateChatRequest.cs similarity index 59% rename from backend/src/Modules/Chats/Application/DTOs/CreateChatRequest.cs rename to backend/src/Modules/Conversations/Application/DTOs/CreateChatRequest.cs index 9446c22..638431b 100644 --- a/backend/src/Modules/Chats/Application/DTOs/CreateChatRequest.cs +++ b/backend/src/Modules/Conversations/Application/DTOs/CreateChatRequest.cs @@ -1,7 +1,8 @@ using System; using System.Collections.Generic; -using Knot.Modules.Chats.Domain; +using Knot.Modules.Conversations.Domain; -namespace Knot.Modules.Chats.Application.DTOs; +namespace Knot.Modules.Conversations.Application.DTOs; public sealed record CreateChatRequest(string Name, ChatType Type, List MemberIds); + diff --git a/backend/src/Modules/Chats/Application/DTOs/CreateGroupChatRequest.cs b/backend/src/Modules/Conversations/Application/DTOs/CreateGroupChatRequest.cs similarity index 69% rename from backend/src/Modules/Chats/Application/DTOs/CreateGroupChatRequest.cs rename to backend/src/Modules/Conversations/Application/DTOs/CreateGroupChatRequest.cs index 122feee..8357fc1 100644 --- a/backend/src/Modules/Chats/Application/DTOs/CreateGroupChatRequest.cs +++ b/backend/src/Modules/Conversations/Application/DTOs/CreateGroupChatRequest.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; -namespace Knot.Modules.Chats.Application.DTOs; +namespace Knot.Modules.Conversations.Application.DTOs; public sealed record CreateGroupChatRequest(string Name, List MemberIds); + diff --git a/backend/src/Modules/Conversations/Application/DTOs/CreatePersonalChatRequest.cs b/backend/src/Modules/Conversations/Application/DTOs/CreatePersonalChatRequest.cs new file mode 100644 index 0000000..8878ab5 --- /dev/null +++ b/backend/src/Modules/Conversations/Application/DTOs/CreatePersonalChatRequest.cs @@ -0,0 +1,4 @@ +namespace Knot.Modules.Conversations.Application.DTOs; +using System; + +public record CreatePersonalChatRequest(Guid UserId); diff --git a/backend/src/Modules/Chats/Application/DTOs/MediaDto.cs b/backend/src/Modules/Conversations/Application/DTOs/MediaDto.cs similarity index 69% rename from backend/src/Modules/Chats/Application/DTOs/MediaDto.cs rename to backend/src/Modules/Conversations/Application/DTOs/MediaDto.cs index bf2e24d..c6dc45f 100644 --- a/backend/src/Modules/Chats/Application/DTOs/MediaDto.cs +++ b/backend/src/Modules/Conversations/Application/DTOs/MediaDto.cs @@ -1,6 +1,6 @@ using System; -namespace Knot.Modules.Chats.Application.DTOs; +namespace Knot.Modules.Conversations.Application.DTOs; public record MediaDto( Guid Id, @@ -9,3 +9,4 @@ public record MediaDto( string? Filename, long? Size ); + diff --git a/backend/src/Modules/Chats/Application/DTOs/MessageDetailDto.cs b/backend/src/Modules/Conversations/Application/DTOs/MessageDetailDto.cs similarity index 93% rename from backend/src/Modules/Chats/Application/DTOs/MessageDetailDto.cs rename to backend/src/Modules/Conversations/Application/DTOs/MessageDetailDto.cs index 302662d..686350f 100644 --- a/backend/src/Modules/Chats/Application/DTOs/MessageDetailDto.cs +++ b/backend/src/Modules/Conversations/Application/DTOs/MessageDetailDto.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace Knot.Modules.Chats.Application.DTOs; +namespace Knot.Modules.Conversations.Application.DTOs; public record MessageDetailDto( Guid Id, @@ -41,3 +41,4 @@ public record MessageReactionDto( Guid UserId, MessageSenderDto? User ); + diff --git a/backend/src/Modules/Chats/Application/DTOs/MessageSenderDto.cs b/backend/src/Modules/Conversations/Application/DTOs/MessageSenderDto.cs similarity index 69% rename from backend/src/Modules/Chats/Application/DTOs/MessageSenderDto.cs rename to backend/src/Modules/Conversations/Application/DTOs/MessageSenderDto.cs index 55c31ae..c1c996e 100644 --- a/backend/src/Modules/Chats/Application/DTOs/MessageSenderDto.cs +++ b/backend/src/Modules/Conversations/Application/DTOs/MessageSenderDto.cs @@ -1,6 +1,6 @@ using System; -namespace Knot.Modules.Chats.Application.DTOs; +namespace Knot.Modules.Conversations.Application.DTOs; public record MessageSenderDto( Guid Id, @@ -8,3 +8,4 @@ public record MessageSenderDto( string DisplayName, string? Avatar ); + diff --git a/backend/src/Modules/Chats/Application/DTOs/ReactionDto.cs b/backend/src/Modules/Conversations/Application/DTOs/ReactionDto.cs similarity index 68% rename from backend/src/Modules/Chats/Application/DTOs/ReactionDto.cs rename to backend/src/Modules/Conversations/Application/DTOs/ReactionDto.cs index 988bf86..d427841 100644 --- a/backend/src/Modules/Chats/Application/DTOs/ReactionDto.cs +++ b/backend/src/Modules/Conversations/Application/DTOs/ReactionDto.cs @@ -1,6 +1,6 @@ using System; -namespace Knot.Modules.Chats.Application.DTOs; +namespace Knot.Modules.Conversations.Application.DTOs; public record ReactionDto( Guid Id, @@ -8,3 +8,4 @@ public record ReactionDto( Guid UserId, MessageSenderDto User ); + diff --git a/backend/src/Modules/Chats/Application/DTOs/ReadByDto.cs b/backend/src/Modules/Conversations/Application/DTOs/ReadByDto.cs similarity index 51% rename from backend/src/Modules/Chats/Application/DTOs/ReadByDto.cs rename to backend/src/Modules/Conversations/Application/DTOs/ReadByDto.cs index 0f853d0..2443891 100644 --- a/backend/src/Modules/Chats/Application/DTOs/ReadByDto.cs +++ b/backend/src/Modules/Conversations/Application/DTOs/ReadByDto.cs @@ -1,7 +1,8 @@ using System; -namespace Knot.Modules.Chats.Application.DTOs; +namespace Knot.Modules.Conversations.Application.DTOs; public record ReadByDto( Guid UserId ); + diff --git a/backend/src/Modules/Chats/Application/DTOs/SearchMessageDto.cs b/backend/src/Modules/Conversations/Application/DTOs/SearchMessageDto.cs similarity index 91% rename from backend/src/Modules/Chats/Application/DTOs/SearchMessageDto.cs rename to backend/src/Modules/Conversations/Application/DTOs/SearchMessageDto.cs index 82758a7..cc4744d 100644 --- a/backend/src/Modules/Chats/Application/DTOs/SearchMessageDto.cs +++ b/backend/src/Modules/Conversations/Application/DTOs/SearchMessageDto.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace Knot.Modules.Chats.Application.DTOs; +namespace Knot.Modules.Conversations.Application.DTOs; public record SearchMessageDto( Guid Id, @@ -30,3 +30,4 @@ public record SimpleReactionDto( Guid UserId, string Emoji ); + diff --git a/backend/src/Modules/Chats/Application/DTOs/SendMessageRequest.cs b/backend/src/Modules/Conversations/Application/DTOs/SendMessageRequest.cs similarity index 86% rename from backend/src/Modules/Chats/Application/DTOs/SendMessageRequest.cs rename to backend/src/Modules/Conversations/Application/DTOs/SendMessageRequest.cs index 7cbfa31..7e93202 100644 --- a/backend/src/Modules/Chats/Application/DTOs/SendMessageRequest.cs +++ b/backend/src/Modules/Conversations/Application/DTOs/SendMessageRequest.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace Knot.Modules.Chats.Application.DTOs; +namespace Knot.Modules.Conversations.Application.DTOs; public sealed record SendMessageRequest( string? Content, @@ -12,3 +12,4 @@ public sealed record SendMessageRequest( Guid? ForwardedFromId = null); public sealed record AttachmentDto(string Type, string Url, string? FileName, long? FileSize); + diff --git a/backend/src/Modules/Chats/Application/DTOs/SharedMediaDto.cs b/backend/src/Modules/Conversations/Application/DTOs/SharedMediaDto.cs similarity index 86% rename from backend/src/Modules/Chats/Application/DTOs/SharedMediaDto.cs rename to backend/src/Modules/Conversations/Application/DTOs/SharedMediaDto.cs index 8914fd5..917406b 100644 --- a/backend/src/Modules/Chats/Application/DTOs/SharedMediaDto.cs +++ b/backend/src/Modules/Conversations/Application/DTOs/SharedMediaDto.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace Knot.Modules.Chats.Application.DTOs; +namespace Knot.Modules.Conversations.Application.DTOs; public record SharedMediaDto( Guid Id, @@ -18,3 +18,4 @@ public record SharedMediaDto( string? Type, List? Media ); + diff --git a/backend/src/Modules/Conversations/Application/DTOs/TogglePinResponse.cs b/backend/src/Modules/Conversations/Application/DTOs/TogglePinResponse.cs new file mode 100644 index 0000000..173e97d --- /dev/null +++ b/backend/src/Modules/Conversations/Application/DTOs/TogglePinResponse.cs @@ -0,0 +1,4 @@ +namespace Knot.Modules.Conversations.Application.DTOs; + +public record TogglePinResponse(bool IsPinned); + diff --git a/backend/src/Modules/Chats/Application/DTOs/UpdateChatRequest.cs b/backend/src/Modules/Conversations/Application/DTOs/UpdateChatRequest.cs similarity index 57% rename from backend/src/Modules/Chats/Application/DTOs/UpdateChatRequest.cs rename to backend/src/Modules/Conversations/Application/DTOs/UpdateChatRequest.cs index 2d95905..e361993 100644 --- a/backend/src/Modules/Chats/Application/DTOs/UpdateChatRequest.cs +++ b/backend/src/Modules/Conversations/Application/DTOs/UpdateChatRequest.cs @@ -1,3 +1,4 @@ -namespace Knot.Modules.Chats.Application.DTOs; +namespace Knot.Modules.Conversations.Application.DTOs; public sealed record UpdateChatRequest(string? Name, string? Description); + diff --git a/backend/src/Modules/Chats/Application/DTOs/UploadFileResponseDto.cs b/backend/src/Modules/Conversations/Application/DTOs/UploadFileResponseDto.cs similarity index 71% rename from backend/src/Modules/Chats/Application/DTOs/UploadFileResponseDto.cs rename to backend/src/Modules/Conversations/Application/DTOs/UploadFileResponseDto.cs index 1ee6f95..fb5b995 100644 --- a/backend/src/Modules/Chats/Application/DTOs/UploadFileResponseDto.cs +++ b/backend/src/Modules/Conversations/Application/DTOs/UploadFileResponseDto.cs @@ -1,10 +1,11 @@ using System; using System.Collections.Generic; -namespace Knot.Modules.Chats.Application.DTOs; +namespace Knot.Modules.Conversations.Application.DTOs; public record UploadFileResponseDto( string Url, string Filename, long Size ); + diff --git a/backend/src/Modules/Chats/Application/Messages/Delete/DeleteMessagesCommand.cs b/backend/src/Modules/Conversations/Application/Messages/Delete/DeleteMessagesCommand.cs similarity index 87% rename from backend/src/Modules/Chats/Application/Messages/Delete/DeleteMessagesCommand.cs rename to backend/src/Modules/Conversations/Application/Messages/Delete/DeleteMessagesCommand.cs index 1106119..b0a2563 100644 --- a/backend/src/Modules/Chats/Application/Messages/Delete/DeleteMessagesCommand.cs +++ b/backend/src/Modules/Conversations/Application/Messages/Delete/DeleteMessagesCommand.cs @@ -1,11 +1,13 @@ +using Knot.Modules.Messaging.Application.Abstractions; +using Knot.Modules.Messaging.Domain; using MediatR; using Microsoft.AspNetCore.SignalR; -using global::Knot.Modules.Chats.Domain; -using global::Knot.Modules.Chats.Infrastructure.SignalR; +using global::Knot.Modules.Conversations.Domain; +using global::Knot.Modules.Conversations.Infrastructure.SignalR; using global::Knot.Shared.Kernel; -using global::Knot.Modules.Chats.Application.Abstractions; +using global::Knot.Modules.Conversations.Application.Abstractions; -namespace Knot.Modules.Chats.Application.Messages.Delete; +namespace Knot.Modules.Conversations.Application.Messages.Delete; public sealed record DeleteMessagesCommand( Guid ChatId, @@ -77,3 +79,5 @@ public sealed class DeleteMessagesCommandHandler : ICommandHandler>; @@ -145,3 +147,5 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler>; @@ -125,3 +127,5 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler>; @@ -66,3 +68,5 @@ internal sealed class SearchMessagesQueryHandler : IQueryHandler /// Команда для отправки сообщения в чат. @@ -133,3 +135,5 @@ public sealed class SendMessageCommandHandler : ICommandHandler; @@ -42,3 +42,4 @@ internal sealed class UploadFileCommandHandler : ICommandHandler + /// ╨а╨╡╨│╨╕╤Б╤В╤А╨░╤Ж╨╕╤П ╤Б╨╡╤А╨▓╨╕╤Б╨╛╨▓ ╨╝╨╛╨┤╤Г╨╗╤П Chats. + /// + public static IServiceCollection AddConversationsModule( + this IServiceCollection services, + IConfiguration configuration) + { + // ╨Э╨░╤Б╤В╤А╨╛╨╣╨║╨░ ╨▒╨░╨╖╤Л ╨┤╨░╨╜╨╜╤Л╤Е + string connectionString = configuration.GetConnectionString("DefaultConnection")!; + + services.AddDbContext(options => + options.UseNpgsql(connectionString)); + + // MongoDB Setup for Messages + + var mongoConnectionString = configuration.GetConnectionString("MongoConnection") ?? "mongodb://localhost:27017"; + + // Registration + services.AddScoped(sp => sp.GetRequiredService()); + services.AddScoped(); + + // MediatR + services.AddMediatR(config => + config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly)); + + services.AddScoped(); + services.AddScoped(); + return services; + } +} \ No newline at end of file diff --git a/backend/src/Modules/Chats/Domain/Chat.cs b/backend/src/Modules/Conversations/Domain/Chat.cs similarity index 98% rename from backend/src/Modules/Chats/Domain/Chat.cs rename to backend/src/Modules/Conversations/Domain/Chat.cs index 1e8cc24..73417e9 100644 --- a/backend/src/Modules/Chats/Domain/Chat.cs +++ b/backend/src/Modules/Conversations/Domain/Chat.cs @@ -1,6 +1,6 @@ using Knot.Shared.Kernel; -namespace Knot.Modules.Chats.Domain; +namespace Knot.Modules.Conversations.Domain; public sealed record ChatCreatedDomainEvent(Chat Chat) : IDomainEvent; public sealed record ChatMemberAddedDomainEvent(Guid ChatId, Guid UserId) : IDomainEvent; @@ -154,3 +154,4 @@ public sealed class ChatMember : Entity LastDeliveredMessageId = messageId; } } + diff --git a/backend/src/Modules/Chats/Domain/ChatConstants.cs b/backend/src/Modules/Conversations/Domain/ChatConstants.cs similarity index 83% rename from backend/src/Modules/Chats/Domain/ChatConstants.cs rename to backend/src/Modules/Conversations/Domain/ChatConstants.cs index 3faae01..33e0890 100644 --- a/backend/src/Modules/Chats/Domain/ChatConstants.cs +++ b/backend/src/Modules/Conversations/Domain/ChatConstants.cs @@ -1,4 +1,4 @@ -namespace Knot.Modules.Chats.Domain; +namespace Knot.Modules.Conversations.Domain; public static class ChatConstants { @@ -7,3 +7,4 @@ public static class ChatConstants public const int SearchMessagesLimit = 50; public const int MaxFileUploadSizeMb = 50; } + diff --git a/backend/src/Modules/Chats/Domain/ChatErrors.cs b/backend/src/Modules/Conversations/Domain/ChatErrors.cs similarity index 97% rename from backend/src/Modules/Chats/Domain/ChatErrors.cs rename to backend/src/Modules/Conversations/Domain/ChatErrors.cs index b48bd62..0877f6b 100644 --- a/backend/src/Modules/Chats/Domain/ChatErrors.cs +++ b/backend/src/Modules/Conversations/Domain/ChatErrors.cs @@ -1,6 +1,6 @@ using Knot.Shared.Kernel; -namespace Knot.Modules.Chats.Domain; +namespace Knot.Modules.Conversations.Domain; public static class ChatErrors { @@ -19,3 +19,4 @@ public static class ChatErrors public static Error ImportCreateChatFailed(string msg) => new Error("Import.CreateChatFailed", msg); public static Error FileTooLarge(int maxMb) => new Error("File.TooLarge", $"File exceeds the maximum allowed size of {maxMb}MB."); } + diff --git a/backend/src/Modules/Chats/Domain/IChatRepository.cs b/backend/src/Modules/Conversations/Domain/IChatRepository.cs similarity index 81% rename from backend/src/Modules/Chats/Domain/IChatRepository.cs rename to backend/src/Modules/Conversations/Domain/IChatRepository.cs index 97c9597..4eb7698 100644 --- a/backend/src/Modules/Chats/Domain/IChatRepository.cs +++ b/backend/src/Modules/Conversations/Domain/IChatRepository.cs @@ -1,6 +1,6 @@ -using Knot.Modules.Chats.Domain; +using Knot.Modules.Conversations.Domain; -namespace Knot.Modules.Chats.Domain; +namespace Knot.Modules.Conversations.Domain; public interface IChatRepository { @@ -11,3 +11,4 @@ public interface IChatRepository Task GetFavoritesAsync(Guid userId, CancellationToken cancellationToken); Task> GetUserChatsAsync(Guid userId, CancellationToken cancellationToken); } + diff --git a/backend/src/Modules/Chats/Infrastructure/Handlers/ChatCreatedDomainEventHandler.cs b/backend/src/Modules/Conversations/Infrastructure/Handlers/ChatCreatedDomainEventHandler.cs similarity index 96% rename from backend/src/Modules/Chats/Infrastructure/Handlers/ChatCreatedDomainEventHandler.cs rename to backend/src/Modules/Conversations/Infrastructure/Handlers/ChatCreatedDomainEventHandler.cs index 27aa7a5..4dd2403 100644 --- a/backend/src/Modules/Chats/Infrastructure/Handlers/ChatCreatedDomainEventHandler.cs +++ b/backend/src/Modules/Conversations/Infrastructure/Handlers/ChatCreatedDomainEventHandler.cs @@ -1,10 +1,10 @@ using Knot.Shared.Kernel; -using Knot.Modules.Chats.Domain; -using Knot.Modules.Chats.Infrastructure.SignalR; +using Knot.Modules.Conversations.Domain; +using Knot.Modules.Conversations.Infrastructure.SignalR; using MediatR; using Microsoft.AspNetCore.SignalR; -namespace Knot.Modules.Chats.Infrastructure.Handlers; +namespace Knot.Modules.Conversations.Infrastructure.Handlers; /// /// Обработчик доменного события создания чата. @@ -89,3 +89,4 @@ public sealed class ChatCreatedDomainEventHandler : INotificationHandler /// Контекст базы данных для модуля чатов. @@ -85,3 +85,4 @@ public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork return result; } } + diff --git a/backend/src/Modules/Conversations/Infrastructure/Services/ChatAccessProvider.cs b/backend/src/Modules/Conversations/Infrastructure/Services/ChatAccessProvider.cs new file mode 100644 index 0000000..b7f1a7e --- /dev/null +++ b/backend/src/Modules/Conversations/Infrastructure/Services/ChatAccessProvider.cs @@ -0,0 +1,11 @@ +namespace Knot.Modules.Conversations.Infrastructure.Services; +using Knot.Modules.Messaging.Application.Abstractions; +using Knot.Modules.Conversations.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +public class ChatAccessProvider : IChatAccessProvider { private readonly ChatsDbContext _db; public ChatAccessProvider(ChatsDbContext db) { _db = db; } public Task> GetValidChatIdsForUserAsync(Guid userId, CancellationToken ct) { return _db.Chats.Where(c => c.Members.Any(m => m.UserId == userId)).Select(c => c.Id).ToListAsync(ct); } } + diff --git a/backend/src/Modules/Chats/Infrastructure/SignalR/ChatHub.cs b/backend/src/Modules/Conversations/Infrastructure/SignalR/ChatHub.cs similarity index 98% rename from backend/src/Modules/Chats/Infrastructure/SignalR/ChatHub.cs rename to backend/src/Modules/Conversations/Infrastructure/SignalR/ChatHub.cs index 31b1e38..7de3736 100644 --- a/backend/src/Modules/Chats/Infrastructure/SignalR/ChatHub.cs +++ b/backend/src/Modules/Conversations/Infrastructure/SignalR/ChatHub.cs @@ -4,15 +4,15 @@ using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; -using Knot.Modules.Chats.Application.Messages.Send; -using Knot.Modules.Chats.Application.Messages.Read; -using Knot.Modules.Chats.Application.Messages.Delete; -using Knot.Modules.Chats.Application.Messages.React; -using Knot.Modules.Chats.Domain; +using Knot.Modules.Conversations.Application.Messages.Send; +using Knot.Modules.Conversations.Application.Messages.Read; +using Knot.Modules.Conversations.Application.Messages.Delete; +using Knot.Modules.Conversations.Application.Messages.React; +using Knot.Modules.Conversations.Domain; using Knot.Shared.Kernel; using Microsoft.Extensions.Caching.Memory; -namespace Knot.Modules.Chats.Infrastructure.SignalR; +namespace Knot.Modules.Conversations.Infrastructure.SignalR; /// /// Хаб SignalR для обработки сообщений и WebRTC сигналинга в реальном времени. @@ -661,3 +661,4 @@ public sealed class ChatHub : Hub public record GroupRenegotiateAnswerRequest(string ChatId, string TargetUserId, object Answer); public record FriendSignalRequest(string FriendId); } + diff --git a/backend/src/Modules/Conversations/Infrastructure/SignalR/MessageNotifier.cs b/backend/src/Modules/Conversations/Infrastructure/SignalR/MessageNotifier.cs new file mode 100644 index 0000000..e55d921 --- /dev/null +++ b/backend/src/Modules/Conversations/Infrastructure/SignalR/MessageNotifier.cs @@ -0,0 +1,7 @@ +namespace Knot.Modules.Conversations.Infrastructure.SignalR; +using Knot.Modules.Messaging.Application.Abstractions; +using Microsoft.AspNetCore.SignalR; +using System; +using System.Threading; +using System.Threading.Tasks; +public class MessageNotifier : IMessageNotifier { private readonly IHubContext _hubContext; public MessageNotifier(IHubContext hubContext) { _hubContext = hubContext; } public Task NotifyNewMessageAsync(Guid chatId, object messagePayload, CancellationToken cancellationToken) { return _hubContext.Clients.Group(chatId.ToString()).SendAsync("new_message", messagePayload, cancellationToken); } } \ No newline at end of file diff --git a/backend/src/Modules/Chats/Knot.Modules.Chats.csproj b/backend/src/Modules/Conversations/Knot.Modules.Conversations.csproj similarity index 91% rename from backend/src/Modules/Chats/Knot.Modules.Chats.csproj rename to backend/src/Modules/Conversations/Knot.Modules.Conversations.csproj index 71d2902..8d94180 100644 --- a/backend/src/Modules/Chats/Knot.Modules.Chats.csproj +++ b/backend/src/Modules/Conversations/Knot.Modules.Conversations.csproj @@ -2,10 +2,12 @@ + + @@ -33,3 +35,4 @@ + diff --git a/backend/src/Modules/Chats/Migrations/20260320185319_AddHighWaterMark.Designer.cs b/backend/src/Modules/Conversations/Migrations/20260322191932_InitialConversations.Designer.cs similarity index 88% rename from backend/src/Modules/Chats/Migrations/20260320185319_AddHighWaterMark.Designer.cs rename to backend/src/Modules/Conversations/Migrations/20260322191932_InitialConversations.Designer.cs index 530e2ff..3ae1c4d 100644 --- a/backend/src/Modules/Chats/Migrations/20260320185319_AddHighWaterMark.Designer.cs +++ b/backend/src/Modules/Conversations/Migrations/20260322191932_InitialConversations.Designer.cs @@ -1,6 +1,6 @@ // using System; -using Knot.Modules.Chats.Infrastructure.Persistence; +using Knot.Modules.Conversations.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -9,11 +9,11 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable -namespace Knot.Modules.Chats.Migrations +namespace Knot.Modules.Conversations.Migrations { [DbContext(typeof(ChatsDbContext))] - [Migration("20260320185319_AddHighWaterMark")] - partial class AddHighWaterMark + [Migration("20260322191932_InitialConversations")] + partial class InitialConversations { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -26,7 +26,7 @@ namespace Knot.Modules.Chats.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b => + modelBuilder.Entity("Knot.Modules.Conversations.Domain.Chat", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -56,9 +56,9 @@ namespace Knot.Modules.Chats.Migrations b.ToTable("Chats", "chats"); }); - modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b => + modelBuilder.Entity("Knot.Modules.Conversations.Domain.Chat", b => { - b.OwnsMany("Knot.Modules.Chats.Domain.ChatMember", "Members", b1 => + b.OwnsMany("Knot.Modules.Conversations.Domain.ChatMember", "Members", b1 => { b1.Property("Id") .ValueGeneratedOnAdd() diff --git a/backend/src/Modules/Chats/Migrations/20260319124845_InitialChats.cs b/backend/src/Modules/Conversations/Migrations/20260322191932_InitialConversations.cs similarity index 84% rename from backend/src/Modules/Chats/Migrations/20260319124845_InitialChats.cs rename to backend/src/Modules/Conversations/Migrations/20260322191932_InitialConversations.cs index d32bc50..9969cfe 100644 --- a/backend/src/Modules/Chats/Migrations/20260319124845_InitialChats.cs +++ b/backend/src/Modules/Conversations/Migrations/20260322191932_InitialConversations.cs @@ -3,10 +3,10 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace Knot.Modules.Chats.Migrations +namespace Knot.Modules.Conversations.Migrations { /// - public partial class InitialChats : Migration + public partial class InitialConversations : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -24,7 +24,8 @@ namespace Knot.Modules.Chats.Migrations Name = table.Column(type: "text", nullable: true), Description = table.Column(type: "text", nullable: true), Avatar = table.Column(type: "text", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + LastMessageSequenceId = table.Column(type: "bigint", nullable: false) }, constraints: table => { @@ -42,7 +43,10 @@ namespace Knot.Modules.Chats.Migrations Role = table.Column(type: "text", nullable: false), JoinedAt = table.Column(type: "timestamp with time zone", nullable: false), IsPinned = table.Column(type: "boolean", nullable: false), - IsMuted = table.Column(type: "boolean", nullable: false) + IsMuted = table.Column(type: "boolean", nullable: false), + LastReadMessageId = table.Column(type: "uuid", nullable: true), + LastReadSequenceId = table.Column(type: "bigint", nullable: false), + LastDeliveredMessageId = table.Column(type: "uuid", nullable: true) }, constraints: table => { diff --git a/backend/src/Modules/Chats/Migrations/ChatsDbContextModelSnapshot.cs b/backend/src/Modules/Conversations/Migrations/ChatsDbContextModelSnapshot.cs similarity index 90% rename from backend/src/Modules/Chats/Migrations/ChatsDbContextModelSnapshot.cs rename to backend/src/Modules/Conversations/Migrations/ChatsDbContextModelSnapshot.cs index 3d5500d..b0df6c5 100644 --- a/backend/src/Modules/Chats/Migrations/ChatsDbContextModelSnapshot.cs +++ b/backend/src/Modules/Conversations/Migrations/ChatsDbContextModelSnapshot.cs @@ -1,6 +1,6 @@ // using System; -using Knot.Modules.Chats.Infrastructure.Persistence; +using Knot.Modules.Conversations.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -8,7 +8,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable -namespace Knot.Modules.Chats.Migrations +namespace Knot.Modules.Conversations.Migrations { [DbContext(typeof(ChatsDbContext))] partial class ChatsDbContextModelSnapshot : ModelSnapshot @@ -23,7 +23,7 @@ namespace Knot.Modules.Chats.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b => + modelBuilder.Entity("Knot.Modules.Conversations.Domain.Chat", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -53,9 +53,9 @@ namespace Knot.Modules.Chats.Migrations b.ToTable("Chats", "chats"); }); - modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b => + modelBuilder.Entity("Knot.Modules.Conversations.Domain.Chat", b => { - b.OwnsMany("Knot.Modules.Chats.Domain.ChatMember", "Members", b1 => + b.OwnsMany("Knot.Modules.Conversations.Domain.ChatMember", "Members", b1 => { b1.Property("Id") .ValueGeneratedOnAdd() diff --git a/backend/src/Modules/Conversations/Presentation/Endpoints/ChatsEndpoints.cs b/backend/src/Modules/Conversations/Presentation/Endpoints/ChatsEndpoints.cs new file mode 100644 index 0000000..0665923 --- /dev/null +++ b/backend/src/Modules/Conversations/Presentation/Endpoints/ChatsEndpoints.cs @@ -0,0 +1,174 @@ +using Carter; +using Knot.Shared.Kernel; +using Knot.Modules.Conversations.Application.DTOs; +using Knot.Modules.Conversations.Application.Chats.GetChats; +using Knot.Modules.Conversations.Application.Chats.GetChatById; +using Knot.Modules.Conversations.Application.Chats.Create; +using Knot.Modules.Conversations.Application.Chats.GetOrCreateFavorites; +using Knot.Modules.Conversations.Application.Chats.Update; +using Knot.Modules.Conversations.Application.Chats.LeaveOrDelete; +using Knot.Modules.Conversations.Application.Chats.Clear; +using Knot.Modules.Conversations.Application.Chats.TogglePin; +using Knot.Modules.Conversations.Application.Chats.Members; +using Knot.Modules.Conversations.Application.Chats.Avatar; +using Knot.Modules.Conversations.Domain; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Mvc; + +namespace Knot.Modules.Conversations.Presentation.Endpoints; + +public sealed class ChatsEndpoints : ICarterModule +{ + public void AddRoutes(IEndpointRouteBuilder app) + { + var group = app.MapGroup("api/chats").RequireAuthorization(); + + group.MapGet("", async (ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new GetChatsQuery(userContext.UserId), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error.Description); + }); + + group.MapPost("", async ([FromBody] CreateChatRequest request, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var command = new CreateChatCommand(request.Name, request.Type, request.MemberIds); + var result = await sender.Send(command, ct); + if (result.IsFailure) return Results.BadRequest(result.Error.Description); + + var chatResult = await sender.Send(new GetChatByIdQuery(userContext.UserId, result.Value), ct); + return Results.Ok(chatResult.Value); + }); + + group.MapPost("personal", async ([FromBody] CreatePersonalChatRequest request, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var command = new CreateChatCommand(string.Empty, ChatType.Personal, new List { userContext.UserId, request.UserId }); + var result = await sender.Send(command, ct); + if (result.IsFailure) return Results.BadRequest(result.Error.Description); + + var chatResult = await sender.Send(new GetChatByIdQuery(userContext.UserId, result.Value), ct); + return Results.Ok(chatResult.Value); + }); + + group.MapPost("group", async ([FromBody] CreateGroupChatRequest request, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var memberIds = request.MemberIds.ToList(); + if (memberIds.Contains(userContext.UserId)) memberIds.Remove(userContext.UserId); + memberIds.Insert(0, userContext.UserId); + + var command = new CreateChatCommand(request.Name, ChatType.Group, memberIds); + var result = await sender.Send(command, ct); + if (result.IsFailure) return Results.BadRequest(result.Error.Description); + + var chatResult = await sender.Send(new GetChatByIdQuery(userContext.UserId, result.Value), ct); + return Results.Ok(chatResult.Value); + }); + + group.MapPost("favorites", async (ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new GetOrCreateFavoritesCommand(userContext.UserId), ct); + if (result.IsFailure) return Results.BadRequest(result.Error.Description); + + var chatResult = await sender.Send(new GetChatByIdQuery(userContext.UserId, result.Value), ct); + return Results.Ok(chatResult.Value); + }); + + group.MapPut("{id:guid}", async (Guid id, [FromBody] UpdateChatRequest request, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new UpdateChatCommand(id, userContext.UserId, request.Name, request.Description), ct); + if (result.IsFailure) return Results.NotFound(); + + var chatResult = await sender.Send(new GetChatByIdQuery(userContext.UserId, result.Value), ct); + return Results.Ok(chatResult.Value); + }); + + group.MapDelete("{id:guid}", async (Guid id, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new LeaveOrDeleteChatCommand(id, userContext.UserId), ct); + if (result.IsFailure) + { + if (result.Error.Code == "Unauthorized") return Results.Forbid(); + return Results.NotFound(); + } + return Results.Ok(result.Value); + }); + + group.MapPost("{id:guid}/clear", async (Guid id, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new ClearChatCommand(id, userContext.UserId), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(); + }); + + group.MapPost("{id:guid}/pin", async (Guid id, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new TogglePinCommand(id, userContext.UserId), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(); + }); + + group.MapPost("{id:guid}/members", async (Guid id, [FromBody] AddMembersRequest request, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new AddMembersCommand(id, userContext.UserId, request.UserIds.ToList()), ct); + if (result.IsFailure) return Results.NotFound(); + + var chatResult = await sender.Send(new GetChatByIdQuery(userContext.UserId, result.Value), ct); + return Results.Ok(chatResult.Value); + }); + + group.MapDelete("{id:guid}/members/{userId:guid}", async (Guid id, Guid userId, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new RemoveMemberCommand(id, userContext.UserId, userId), ct); + if (result.IsFailure) return Results.NotFound(); + + var chatResult = await sender.Send(new GetChatByIdQuery(userContext.UserId, result.Value), ct); + return Results.Ok(chatResult.Value); + }); + + group.MapPost("{id:guid}/avatar", async (Guid id, HttpRequest req, ISender sender, IUserContext userContext, CancellationToken ct) => + { + if (!req.HasFormContentType) return Results.BadRequest("No file"); + var form = await req.ReadFormAsync(ct); + var avatar = form.Files.FirstOrDefault(); + if (avatar == null || avatar.Length == 0) return Results.BadRequest("No file"); + + using var stream = avatar.OpenReadStream(); + var result = await sender.Send(new UploadGroupAvatarCommand(id, userContext.UserId, avatar.FileName, avatar.ContentType, stream), ct); + if (result.IsFailure) return Results.NotFound(); + + var chatResult = await sender.Send(new GetChatByIdQuery(userContext.UserId, result.Value), ct); + return Results.Ok(chatResult.Value); + }).DisableAntiforgery(); + + group.MapPost("{id:guid}/avatar/crop", async (Guid id, HttpRequest req, ISender sender, IUserContext userContext, CancellationToken ct) => + { + if (!req.HasFormContentType) return Results.BadRequest("No file"); + var form = await req.ReadFormAsync(ct); + var avatar = form.Files.FirstOrDefault(); + if (avatar == null || avatar.Length == 0) return Results.BadRequest("No file"); + + int.TryParse(form["x"], out int x); + int.TryParse(form["y"], out int y); + int.TryParse(form["width"], out int width); + int.TryParse(form["height"], out int height); + + using var stream = avatar.OpenReadStream(); + var result = await sender.Send(new CropGroupAvatarCommand(id, userContext.UserId, avatar.FileName ?? "avatar.jpg", avatar.ContentType, stream, x, y, width, height), ct); + if (result.IsFailure) return Results.NotFound(); + + var chatResult = await sender.Send(new GetChatByIdQuery(userContext.UserId, result.Value), ct); + return Results.Ok(chatResult.Value); + }).DisableAntiforgery(); + + group.MapDelete("{id:guid}/avatar", async (Guid id, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new RemoveGroupAvatarCommand(id, userContext.UserId), ct); + if (result.IsFailure) return Results.NotFound(); + + var chatResult = await sender.Send(new GetChatByIdQuery(userContext.UserId, result.Value), ct); + return Results.Ok(chatResult.Value); + }); + } +} + + diff --git a/backend/src/Modules/Conversations/Presentation/Endpoints/MessagesEndpoints.cs b/backend/src/Modules/Conversations/Presentation/Endpoints/MessagesEndpoints.cs new file mode 100644 index 0000000..9d3bf53 --- /dev/null +++ b/backend/src/Modules/Conversations/Presentation/Endpoints/MessagesEndpoints.cs @@ -0,0 +1,79 @@ +using Carter; +using Knot.Shared.Kernel; +using Knot.Modules.Conversations.Application.DTOs; +using Knot.Modules.Conversations.Application.Messages.GetMessages; +using Knot.Modules.Conversations.Application.Messages.SearchMessages; +using Knot.Modules.Conversations.Application.Messages.UploadFile; +using Knot.Modules.Conversations.Application.Messages.GetSharedMedia; +using Knot.Modules.Conversations.Application.Messages.Send; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Mvc; + +namespace Knot.Modules.Conversations.Presentation.Endpoints; + +public sealed class MessagesEndpoints : ICarterModule +{ + public void AddRoutes(IEndpointRouteBuilder app) + { + var group = app.MapGroup("api/messages").RequireAuthorization(); + + group.MapGet("chat/{chatId:guid}", async (Guid chatId, [FromQuery] string? cursor, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new GetMessagesQuery(userContext.UserId, chatId, cursor), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error.Description); + }); + + group.MapGet("search", async ([FromQuery] string q, [FromQuery] Guid? chatId, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new SearchMessagesQuery(userContext.UserId, q, chatId), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error.Description); + }); + + group.MapPost("upload", async (HttpRequest req, ISender sender, CancellationToken ct) => + { + if (!req.HasFormContentType) return Results.BadRequest("No file uploaded"); + var form = await req.ReadFormAsync(ct); + var file = form.Files.FirstOrDefault(); + if (file == null || file.Length == 0) return Results.BadRequest("No file uploaded"); + + using var stream = file.OpenReadStream(); + var result = await sender.Send(new UploadFileCommand(file.FileName, file.ContentType, file.Length, stream), ct); + + if (result.IsFailure) + { + if (result.Error.Code == "File.TooLarge") return Results.StatusCode(413); + return Results.BadRequest(result.Error.Description); + } + return Results.Ok(result.Value); + }).DisableAntiforgery(); + + group.MapGet("chat/{chatId:guid}/shared", async (Guid chatId, [FromQuery] string? type, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new GetSharedMediaQuery(userContext.UserId, chatId, type), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error.Description); + }); + + group.MapPost("chat/{chatId:guid}", async (Guid chatId, [FromBody] SendMessageRequest request, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var attachments = request.Attachments?.Select(a => + new AttachmentRequest(a.Type, a.Url, a.FileName, a.FileSize)).ToList(); + + var command = new SendMessageCommand( + chatId, + userContext.UserId, + request.Content, + request.Type, + attachments, + request.ReplyToId, + request.Quote, + request.ForwardedFromId); + + var result = await sender.Send(command, ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error.Description); + }); + } +} + diff --git a/backend/src/Host/Application/Federation/Commands/HandshakeFederationCommand.cs b/backend/src/Modules/Federation/Application/Federation/Commands/HandshakeFederationCommand.cs similarity index 98% rename from backend/src/Host/Application/Federation/Commands/HandshakeFederationCommand.cs rename to backend/src/Modules/Federation/Application/Federation/Commands/HandshakeFederationCommand.cs index fe2d38c..ee9f336 100644 --- a/backend/src/Host/Application/Federation/Commands/HandshakeFederationCommand.cs +++ b/backend/src/Modules/Federation/Application/Federation/Commands/HandshakeFederationCommand.cs @@ -1,4 +1,6 @@ -using Host.Application.Stories; + +using Knot.Modules.Stories.Domain; + using Knot.Shared.Kernel; using Knot.Shared.Kernel.Configuration; using Knot.Shared.Kernel.Constants; @@ -49,4 +51,4 @@ internal sealed class HandshakeFederationCommandHandler : ICommandHandler + + net10.0 + enable + enable + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/Modules/Federation/Presentation/Endpoints/FederationEndpoints.cs b/backend/src/Modules/Federation/Presentation/Endpoints/FederationEndpoints.cs new file mode 100644 index 0000000..a976e15 --- /dev/null +++ b/backend/src/Modules/Federation/Presentation/Endpoints/FederationEndpoints.cs @@ -0,0 +1,41 @@ +using Carter; +using MediatR; +using Knot.Shared.Kernel.Constants; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using System; + +namespace Host.Endpoints; + +/// +/// Регистрация эндпоинтов федерации. +/// +public sealed class FederationEndpoints : ICarterModule +{ + public void AddRoutes(IEndpointRouteBuilder app) + { + var group = app.MapGroup(Routes.ApiFederation); + + group.MapPost("/handshake", async ([FromBody] Host.Application.Federation.Commands.HandshakeRequest request, ISender sender, CancellationToken ct) => + { + var result = await sender.Send(new Host.Application.Federation.Commands.HandshakeFederationCommand(request), ct); + if (result.IsSuccess) + { + return Results.Ok(result.Value); + } + + if (result.Error.Code == "Unauthorized") + { + return Results.Forbid(); + } + if (result.Error.Code == Knot.Shared.Kernel.Constants.Errors.DisabledByAdmin) + { + return Results.StatusCode(503); + } + + return Results.BadRequest(new { error = result.Error.Description }); + }); + } +} \ No newline at end of file diff --git a/backend/src/Modules/Identity/Application/Abstractions/IJwtTokenProvider.cs b/backend/src/Modules/Identity/Application/Abstractions/IJwtTokenProvider.cs deleted file mode 100644 index edaba4a..0000000 --- a/backend/src/Modules/Identity/Application/Abstractions/IJwtTokenProvider.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Knot.Modules.Identity.Domain; - -namespace Knot.Modules.Identity.Application.Abstractions; - -public interface IJwtTokenProvider -{ - string Generate(User user); -} diff --git a/backend/src/Modules/Identity/Application/Users/Avatar/DeleteAvatar.cs b/backend/src/Modules/Identity/Application/Users/Avatar/DeleteAvatar.cs deleted file mode 100644 index 785a622..0000000 --- a/backend/src/Modules/Identity/Application/Users/Avatar/DeleteAvatar.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Knot.Shared.Kernel; -using Knot.Modules.Identity.Domain; -using Knot.Modules.Identity.Application.Abstractions; - -namespace Knot.Modules.Identity.Application.Users.Avatar; - -public sealed record DeleteAvatarCommand(Guid UserId) : ICommand; - -internal sealed class DeleteAvatarCommandHandler : ICommandHandler -{ - private readonly IUserRepository _userRepository; - private readonly IIdentityUnitOfWork _unitOfWork; - - public DeleteAvatarCommandHandler(IUserRepository userRepository, IIdentityUnitOfWork unitOfWork) - { - _userRepository = userRepository; - _unitOfWork = unitOfWork; - } - - public async Task> Handle(DeleteAvatarCommand request, CancellationToken cancellationToken) - { - var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); - if (user == null) - { - return Result.Failure(IdentityErrors.UserNotFound); - } - - user.UpdateAvatar(null); - await _unitOfWork.SaveChangesAsync(cancellationToken); - - var dto = new UserProfileDto( - user.Id, - user.Username, - user.DisplayName, - user.Avatar, - user.Bio, - user.Birthday, - user.CreatedAt - ); - - return Result.Success(dto); - } -} diff --git a/backend/src/Modules/Identity/Application/Users/Avatar/UploadAvatar.cs b/backend/src/Modules/Identity/Application/Users/Avatar/UploadAvatar.cs deleted file mode 100644 index 7f428cb..0000000 --- a/backend/src/Modules/Identity/Application/Users/Avatar/UploadAvatar.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Knot.Shared.Kernel; -using Knot.Modules.Identity.Domain; -using Knot.Modules.Identity.Application.Abstractions; -using Knot.Shared.Kernel.Storage; - -namespace Knot.Modules.Identity.Application.Users.Avatar; - -public sealed record UploadAvatarCommand(Guid UserId, Stream FileStream, string FileName, string ContentType) : ICommand; - -internal sealed class UploadAvatarCommandHandler : ICommandHandler -{ - private readonly IUserRepository _userRepository; - private readonly IIdentityUnitOfWork _unitOfWork; - private readonly IFileStorageService _fileStorage; - - public UploadAvatarCommandHandler(IUserRepository userRepository, IIdentityUnitOfWork unitOfWork, IFileStorageService fileStorage) - { - _userRepository = userRepository; - _unitOfWork = unitOfWork; - _fileStorage = fileStorage; - } - - public async Task> Handle(UploadAvatarCommand request, CancellationToken cancellationToken) - { - var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); - if (user == null) - { - return Result.Failure(IdentityErrors.UserNotFound); - } - - var fileId = await _fileStorage.UploadFileAsync(request.FileStream, request.FileName, request.ContentType); - var avatarUrl = $"/api/files/{fileId}"; - - user.UpdateAvatar(avatarUrl); - await _unitOfWork.SaveChangesAsync(cancellationToken); - - var dto = new UserProfileDto( - user.Id, - user.Username, - user.DisplayName, - user.Avatar, - user.Bio, - user.Birthday, - user.CreatedAt - ); - - return Result.Success(dto); - } -} diff --git a/backend/src/Modules/Identity/Application/Users/GetUser/GetUser.cs b/backend/src/Modules/Identity/Application/Users/GetUser/GetUser.cs deleted file mode 100644 index 2ba6c38..0000000 --- a/backend/src/Modules/Identity/Application/Users/GetUser/GetUser.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Knot.Shared.Kernel; -using Knot.Modules.Identity.Domain; -using Knot.Modules.Identity.Application.Abstractions; - -namespace Knot.Modules.Identity.Application.Users.GetUser; - -public sealed record GetUserQuery(Guid Id) : IQuery; - -internal sealed class GetUserQueryHandler : IQueryHandler -{ - private readonly IUserRepository _userRepository; - - public GetUserQueryHandler(IUserRepository userRepository) - { - _userRepository = userRepository; - } - - public async Task> Handle(GetUserQuery request, CancellationToken cancellationToken) - { - var user = await _userRepository.GetByIdAsync(request.Id, cancellationToken); - if (user == null) - { - return Result.Failure(IdentityErrors.UserNotFound); - } - - var dto = new UserProfileDto( - user.Id, - user.Username, - user.DisplayName, - user.Avatar, - user.Bio, - user.Birthday, - user.CreatedAt, - null, - false, - DateTime.UtcNow - ); - - return Result.Success(dto); - } -} diff --git a/backend/src/Modules/Identity/Application/Users/Search/SearchUsers.cs b/backend/src/Modules/Identity/Application/Users/Search/SearchUsers.cs deleted file mode 100644 index ea526d4..0000000 --- a/backend/src/Modules/Identity/Application/Users/Search/SearchUsers.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Knot.Shared.Kernel; -using Knot.Modules.Identity.Domain; -using Knot.Modules.Identity.Application.Abstractions; - -namespace Knot.Modules.Identity.Application.Users.Search; - -public sealed record SearchUsersQuery(string Query) : IQuery>; - -internal sealed class SearchUsersQueryHandler : IQueryHandler> -{ - private readonly IUserRepository _userRepository; - - public SearchUsersQueryHandler(IUserRepository userRepository) - { - _userRepository = userRepository; - } - - public async Task>> Handle(SearchUsersQuery request, CancellationToken cancellationToken) - { - var users = await _userRepository.SearchUsersAsync(request.Query, cancellationToken); - - var result = users.Select(user => new UserDto( - user.Id, - user.Username, - user.DisplayName, - user.Avatar, - false, - DateTime.UtcNow - )).ToList(); - - return Result.Success(result); - } -} diff --git a/backend/src/Modules/Identity/Application/Users/UpdateSettings/UpdateSettings.cs b/backend/src/Modules/Identity/Application/Users/UpdateSettings/UpdateSettings.cs deleted file mode 100644 index 16c4ae4..0000000 --- a/backend/src/Modules/Identity/Application/Users/UpdateSettings/UpdateSettings.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Knot.Shared.Kernel; -using Knot.Modules.Identity.Domain; -using Knot.Modules.Identity.Application.Abstractions; - -namespace Knot.Modules.Identity.Application.Users.UpdateSettings; - -public sealed record UpdateSettingsCommand(Guid UserId, bool? HideStoryViews) : ICommand; - -internal sealed class UpdateSettingsCommandHandler : ICommandHandler -{ - private readonly IUserRepository _userRepository; - private readonly IIdentityUnitOfWork _unitOfWork; - - public UpdateSettingsCommandHandler(IUserRepository userRepository, IIdentityUnitOfWork unitOfWork) - { - _userRepository = userRepository; - _unitOfWork = unitOfWork; - } - - public async Task> Handle(UpdateSettingsCommand request, CancellationToken cancellationToken) - { - var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); - if (user == null) - { - return Result.Failure(IdentityErrors.UserNotFound); - } - - user.UpdateSettings(request.HideStoryViews ?? user.HideStoryViews); - await _unitOfWork.SaveChangesAsync(cancellationToken); - - var dto = new UserProfileDto( - user.Id, - user.Username, - user.DisplayName, - user.Avatar, - user.Bio, - user.Birthday, - user.CreatedAt, - user.HideStoryViews - ); - - return Result.Success(dto); - } -} diff --git a/backend/src/Modules/Identity/Application/Users/UserDto.cs b/backend/src/Modules/Identity/Application/Users/UserDto.cs deleted file mode 100644 index 66169a2..0000000 --- a/backend/src/Modules/Identity/Application/Users/UserDto.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Knot.Modules.Identity.Application.Users; - -public record UserDto( - Guid Id, - string Username, - string DisplayName, - string? Avatar, - bool IsOnline, - DateTime LastSeen -); diff --git a/backend/src/Modules/Identity/Infrastructure/Persistence/IdentityDbContext.cs b/backend/src/Modules/Identity/Infrastructure/Persistence/IdentityDbContext.cs deleted file mode 100644 index 7d566e9..0000000 --- a/backend/src/Modules/Identity/Infrastructure/Persistence/IdentityDbContext.cs +++ /dev/null @@ -1,157 +0,0 @@ -using MediatR; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; -using Microsoft.EntityFrameworkCore.Metadata; -using Knot.Modules.Identity.Application.Abstractions; -using Knot.Modules.Identity.Domain; -using Knot.Shared.Kernel; -using Knot.Shared.Kernel.Security; - -namespace Knot.Modules.Identity.Infrastructure.Persistence; - -/// -/// Контекст базы данных для модуля Identity. -/// -public sealed class IdentityDbContext : DbContext, IIdentityUnitOfWork, Knot.Modules.Identity.Application.Abstractions.IIdentityDbContext -{ - private readonly IMediator _mediator; - private readonly IEncryptionService _encryptionService; - - public IdentityDbContext(DbContextOptions options, IMediator mediator, IEncryptionService encryptionService) - : base(options) - { - _mediator = mediator; - _encryptionService = encryptionService; - } - - public DbSet Users => Set(); - public DbSet Stories => Set(); - public DbSet StoryViewers => Set(); - public DbSet StoryReactions => Set(); - public DbSet StoryReplies => Set(); - public DbSet Friendships => Set(); - - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - base.OnConfiguring(optionsBuilder); - optionsBuilder.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning)); - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.HasDefaultSchema("identity"); - - modelBuilder.Entity(builder => - { - builder.ToTable("Friendships"); - builder.HasKey(f => f.Id); - builder.HasIndex(f => new { f.UserId, f.FriendId }).IsUnique(); - }); - - modelBuilder.Entity(builder => - { - builder.ToTable("Stories"); - builder.HasKey(s => s.Id); - builder.Property(s => s.Type).IsRequired(); - - builder.Property(s => s.Content) - .HasConversion( - v => v == null ? null : _encryptionService.EncryptMessage(v), - v => v == null ? null : _encryptionService.DecryptMessage(v)! - ); - - // Configure backing fields for collections - - builder.Metadata.FindNavigation(nameof(Story.Viewers))?.SetPropertyAccessMode(PropertyAccessMode.Field); - builder.Metadata.FindNavigation(nameof(Story.Reactions))?.SetPropertyAccessMode(PropertyAccessMode.Field); - builder.Metadata.FindNavigation(nameof(Story.Replies))?.SetPropertyAccessMode(PropertyAccessMode.Field); - - - builder.HasMany(s => s.Viewers) - .WithOne() - .HasForeignKey(v => v.StoryId) - .OnDelete(DeleteBehavior.Cascade); - builder.HasMany(s => s.Reactions) - .WithOne() - .HasForeignKey(r => r.StoryId) - .OnDelete(DeleteBehavior.Cascade); - builder.HasMany(s => s.Replies) - .WithOne() - .HasForeignKey(r => r.StoryId) - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity(builder => - { - builder.ToTable("StoryViewers"); - builder.HasKey(v => v.Id); - builder.HasIndex(v => new { v.StoryId, v.UserId }).IsUnique(); - }); - - modelBuilder.Entity(builder => - { - builder.ToTable("StoryReactions"); - builder.HasKey(r => r.Id); - builder.Property(r => r.Emoji).IsRequired().HasMaxLength(10); - builder.HasIndex(r => new { r.StoryId, r.UserId, r.Emoji }).IsUnique(); - }); - - modelBuilder.Entity(builder => - { - builder.ToTable("StoryReplies"); - builder.HasKey(r => r.Id); - - builder.Property(r => r.Content) - .IsRequired() - .HasMaxLength(500) - .HasConversion( - v => _encryptionService.EncryptMessage(v) ?? string.Empty, - v => _encryptionService.DecryptMessage(v) ?? string.Empty - ); - }); - - modelBuilder.Entity(builder => - { - builder.ToTable("Users"); - builder.HasKey(u => u.Id); - builder.Property(u => u.Username).IsRequired().HasMaxLength(50); - builder.HasIndex(u => u.Username).IsUnique(); - builder.Property(u => u.PasswordHash).IsRequired(); - builder.Property(u => u.DisplayName).HasMaxLength(100); - builder.Property(u => u.Email).HasMaxLength(255); - builder.Property(u => u.Bio).HasMaxLength(500); - builder.Property(u => u.Avatar).HasMaxLength(500); - builder.Property(u => u.Birthday); - builder.Property(u => u.CreatedAt).IsRequired(); - builder.Property(u => u.HideStoryViews).HasDefaultValue(false); - }); - } - - public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) - { - var domainEvents = ChangeTracker - .Entries() - .SelectMany(x => - - { - if (x.Entity is AggregateRoot root) - { - var events = root.GetDomainEvents().ToList(); - root.ClearDomainEvents(); - return events; - } - return Enumerable.Empty(); - }) - .ToList(); - - int result = await base.SaveChangesAsync(cancellationToken); - - foreach (var domainEvent in domainEvents) - { - await _mediator.Publish(domainEvent, cancellationToken); - } - - return result; - } -} diff --git a/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311200839_UpdateIdentityWithNewFieldsAndTables.Designer.cs b/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311200839_UpdateIdentityWithNewFieldsAndTables.Designer.cs deleted file mode 100644 index dcb0491..0000000 --- a/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311200839_UpdateIdentityWithNewFieldsAndTables.Designer.cs +++ /dev/null @@ -1,136 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Knot.Modules.Identity.Infrastructure.Persistence; - -#nullable disable - -namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations -{ - [DbContext(typeof(IdentityDbContext))] - [Migration("20260311200839_UpdateIdentityWithNewFieldsAndTables")] - partial class UpdateIdentityWithNewFieldsAndTables - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasDefaultSchema("identity") - .HasAnnotation("ProductVersion", "10.0.4") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.Friendship", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FriendId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "FriendId") - .IsUnique(); - - b.ToTable("Friendships", "identity"); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.Story", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("BgColor") - .HasColumnType("text"); - - b.Property("Content") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.Property("MediaUrl") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.ToTable("Stories", "identity"); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Avatar") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Bio") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Birthday") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DisplayName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("Email") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("Username") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("Users", "identity"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311200839_UpdateIdentityWithNewFieldsAndTables.cs b/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311200839_UpdateIdentityWithNewFieldsAndTables.cs deleted file mode 100644 index 8c448a7..0000000 --- a/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311200839_UpdateIdentityWithNewFieldsAndTables.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations -{ - /// - public partial class UpdateIdentityWithNewFieldsAndTables : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Avatar", - schema: "identity", - table: "Users", - type: "character varying(500)", - maxLength: 500, - nullable: true); - - migrationBuilder.AddColumn( - name: "Bio", - schema: "identity", - table: "Users", - type: "character varying(500)", - maxLength: 500, - nullable: true); - - migrationBuilder.AddColumn( - name: "Birthday", - schema: "identity", - table: "Users", - type: "timestamp with time zone", - nullable: true); - - migrationBuilder.AddColumn( - name: "CreatedAt", - schema: "identity", - table: "Users", - type: "timestamp with time zone", - nullable: false, - defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); - - migrationBuilder.CreateTable( - name: "Friendships", - schema: "identity", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - FriendId = table.Column(type: "uuid", nullable: false), - Status = table.Column(type: "integer", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Friendships", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Stories", - schema: "identity", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - Type = table.Column(type: "text", nullable: false), - MediaUrl = table.Column(type: "text", nullable: true), - Content = table.Column(type: "text", nullable: true), - BgColor = table.Column(type: "text", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - ExpiresAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Stories", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_Friendships_UserId_FriendId", - schema: "identity", - table: "Friendships", - columns: new[] { "UserId", "FriendId" }, - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Friendships", - schema: "identity"); - - migrationBuilder.DropTable( - name: "Stories", - schema: "identity"); - - migrationBuilder.DropColumn( - name: "Avatar", - schema: "identity", - table: "Users"); - - migrationBuilder.DropColumn( - name: "Bio", - schema: "identity", - table: "Users"); - - migrationBuilder.DropColumn( - name: "Birthday", - schema: "identity", - table: "Users"); - - migrationBuilder.DropColumn( - name: "CreatedAt", - schema: "identity", - table: "Users"); - } - } -} diff --git a/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311215347_AddHideStoryViewsToUser.Designer.cs b/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311215347_AddHideStoryViewsToUser.Designer.cs deleted file mode 100644 index 1022027..0000000 --- a/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311215347_AddHideStoryViewsToUser.Designer.cs +++ /dev/null @@ -1,141 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Knot.Modules.Identity.Infrastructure.Persistence; - -#nullable disable - -namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations -{ - [DbContext(typeof(IdentityDbContext))] - [Migration("20260311215347_AddHideStoryViewsToUser")] - partial class AddHideStoryViewsToUser - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasDefaultSchema("identity") - .HasAnnotation("ProductVersion", "10.0.4") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.Friendship", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FriendId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "FriendId") - .IsUnique(); - - b.ToTable("Friendships", "identity"); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.Story", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("BgColor") - .HasColumnType("text"); - - b.Property("Content") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.Property("MediaUrl") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.ToTable("Stories", "identity"); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Avatar") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Bio") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Birthday") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DisplayName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("Email") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("HideStoryViews") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false); - - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("Username") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("Users", "identity"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311215347_AddHideStoryViewsToUser.cs b/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311215347_AddHideStoryViewsToUser.cs deleted file mode 100644 index 64f0280..0000000 --- a/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311215347_AddHideStoryViewsToUser.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations -{ - /// - public partial class AddHideStoryViewsToUser : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "HideStoryViews", - schema: "identity", - table: "Users", - type: "boolean", - nullable: false, - defaultValue: false); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "HideStoryViews", - schema: "identity", - table: "Users"); - } - } -} diff --git a/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260312174453_AddHideStoryViewsAndFixStoryViewer.Designer.cs b/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260312174453_AddHideStoryViewsAndFixStoryViewer.Designer.cs deleted file mode 100644 index 99d5445..0000000 --- a/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260312174453_AddHideStoryViewsAndFixStoryViewer.Designer.cs +++ /dev/null @@ -1,178 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Knot.Modules.Identity.Infrastructure.Persistence; - -#nullable disable - -namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations -{ - [DbContext(typeof(IdentityDbContext))] - [Migration("20260312174453_AddHideStoryViewsAndFixStoryViewer")] - partial class AddHideStoryViewsAndFixStoryViewer - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasDefaultSchema("identity") - .HasAnnotation("ProductVersion", "10.0.4") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.Friendship", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FriendId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "FriendId") - .IsUnique(); - - b.ToTable("Friendships", "identity"); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.Story", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("BgColor") - .HasColumnType("text"); - - b.Property("Content") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.Property("MediaUrl") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.ToTable("Stories", "identity"); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryViewer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("StoryId") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.Property("ViewedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("StoryId", "UserId") - .IsUnique(); - - b.ToTable("StoryViewers", "identity"); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Avatar") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Bio") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Birthday") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DisplayName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("Email") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("HideStoryViews") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false); - - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("Username") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("Users", "identity"); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryViewer", b => - { - b.HasOne("Knot.Modules.Identity.Domain.Story", null) - .WithMany("Viewers") - .HasForeignKey("StoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.Story", b => - { - b.Navigation("Viewers"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260312174453_AddHideStoryViewsAndFixStoryViewer.cs b/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260312174453_AddHideStoryViewsAndFixStoryViewer.cs deleted file mode 100644 index 32d71ef..0000000 --- a/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260312174453_AddHideStoryViewsAndFixStoryViewer.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations -{ - /// - public partial class AddHideStoryViewsAndFixStoryViewer : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "StoryViewers", - schema: "identity", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - StoryId = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - ViewedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_StoryViewers", x => x.Id); - table.ForeignKey( - name: "FK_StoryViewers_Stories_StoryId", - column: x => x.StoryId, - principalSchema: "identity", - principalTable: "Stories", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_StoryViewers_StoryId_UserId", - schema: "identity", - table: "StoryViewers", - columns: new[] { "StoryId", "UserId" }, - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "StoryViewers", - schema: "identity"); - } - } -} diff --git a/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260313115319_AddStoryReactionsReplies.Designer.cs b/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260313115319_AddStoryReactionsReplies.Designer.cs deleted file mode 100644 index da3c592..0000000 --- a/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260313115319_AddStoryReactionsReplies.Designer.cs +++ /dev/null @@ -1,255 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Knot.Modules.Identity.Infrastructure.Persistence; - -#nullable disable - -namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations -{ - [DbContext(typeof(IdentityDbContext))] - [Migration("20260313115319_AddStoryReactionsReplies")] - partial class AddStoryReactionsReplies - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasDefaultSchema("identity") - .HasAnnotation("ProductVersion", "10.0.4") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.Friendship", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FriendId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "FriendId") - .IsUnique(); - - b.ToTable("Friendships", "identity"); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.Story", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("BgColor") - .HasColumnType("text"); - - b.Property("Content") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.Property("MediaUrl") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.ToTable("Stories", "identity"); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryReaction", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Emoji") - .IsRequired() - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b.Property("StoryId") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("StoryId", "UserId", "Emoji") - .IsUnique(); - - b.ToTable("StoryReactions", "identity"); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryReply", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Content") - .IsRequired() - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("StoryId") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("StoryId"); - - b.ToTable("StoryReplies", "identity"); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryViewer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("StoryId") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.Property("ViewedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("StoryId", "UserId") - .IsUnique(); - - b.ToTable("StoryViewers", "identity"); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Avatar") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Bio") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Birthday") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DisplayName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("Email") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("HideStoryViews") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false); - - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("Username") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("Users", "identity"); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryReaction", b => - { - b.HasOne("Knot.Modules.Identity.Domain.Story", null) - .WithMany("Reactions") - .HasForeignKey("StoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryReply", b => - { - b.HasOne("Knot.Modules.Identity.Domain.Story", null) - .WithMany("Replies") - .HasForeignKey("StoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryViewer", b => - { - b.HasOne("Knot.Modules.Identity.Domain.Story", null) - .WithMany("Viewers") - .HasForeignKey("StoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.Story", b => - { - b.Navigation("Reactions"); - - b.Navigation("Replies"); - - b.Navigation("Viewers"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260313115319_AddStoryReactionsReplies.cs b/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260313115319_AddStoryReactionsReplies.cs deleted file mode 100644 index 8069b35..0000000 --- a/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260313115319_AddStoryReactionsReplies.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations -{ - /// - public partial class AddStoryReactionsReplies : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "StoryReactions", - schema: "identity", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - StoryId = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - Emoji = table.Column(type: "character varying(10)", maxLength: 10, nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_StoryReactions", x => x.Id); - table.ForeignKey( - name: "FK_StoryReactions_Stories_StoryId", - column: x => x.StoryId, - principalSchema: "identity", - principalTable: "Stories", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "StoryReplies", - schema: "identity", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - StoryId = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - Content = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_StoryReplies", x => x.Id); - table.ForeignKey( - name: "FK_StoryReplies_Stories_StoryId", - column: x => x.StoryId, - principalSchema: "identity", - principalTable: "Stories", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_StoryReactions_StoryId_UserId_Emoji", - schema: "identity", - table: "StoryReactions", - columns: new[] { "StoryId", "UserId", "Emoji" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_StoryReplies_StoryId", - schema: "identity", - table: "StoryReplies", - column: "StoryId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "StoryReactions", - schema: "identity"); - - migrationBuilder.DropTable( - name: "StoryReplies", - schema: "identity"); - } - } -} diff --git a/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/IdentityDbContextModelSnapshot.cs b/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/IdentityDbContextModelSnapshot.cs deleted file mode 100644 index f1e727c..0000000 --- a/backend/src/Modules/Identity/Infrastructure/Persistence/Migrations/IdentityDbContextModelSnapshot.cs +++ /dev/null @@ -1,252 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Knot.Modules.Identity.Infrastructure.Persistence; - -#nullable disable - -namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations -{ - [DbContext(typeof(IdentityDbContext))] - partial class IdentityDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasDefaultSchema("identity") - .HasAnnotation("ProductVersion", "10.0.4") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.Friendship", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FriendId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "FriendId") - .IsUnique(); - - b.ToTable("Friendships", "identity"); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.Story", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("BgColor") - .HasColumnType("text"); - - b.Property("Content") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.Property("MediaUrl") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.ToTable("Stories", "identity"); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryReaction", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Emoji") - .IsRequired() - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b.Property("StoryId") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("StoryId", "UserId", "Emoji") - .IsUnique(); - - b.ToTable("StoryReactions", "identity"); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryReply", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Content") - .IsRequired() - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("StoryId") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("StoryId"); - - b.ToTable("StoryReplies", "identity"); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryViewer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("StoryId") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.Property("ViewedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("StoryId", "UserId") - .IsUnique(); - - b.ToTable("StoryViewers", "identity"); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Avatar") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Bio") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Birthday") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DisplayName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("Email") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("HideStoryViews") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false); - - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("Username") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("Users", "identity"); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryReaction", b => - { - b.HasOne("Knot.Modules.Identity.Domain.Story", null) - .WithMany("Reactions") - .HasForeignKey("StoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryReply", b => - { - b.HasOne("Knot.Modules.Identity.Domain.Story", null) - .WithMany("Replies") - .HasForeignKey("StoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryViewer", b => - { - b.HasOne("Knot.Modules.Identity.Domain.Story", null) - .WithMany("Viewers") - .HasForeignKey("StoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Identity.Domain.Story", b => - { - b.Navigation("Reactions"); - - b.Navigation("Replies"); - - b.Navigation("Viewers"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/backend/src/Host/Application/Klipy/Queries/GetTrendingGifsQuery.cs b/backend/src/Modules/Klipy/Application/Klipy/Queries/GetTrendingGifsQuery.cs similarity index 100% rename from backend/src/Host/Application/Klipy/Queries/GetTrendingGifsQuery.cs rename to backend/src/Modules/Klipy/Application/Klipy/Queries/GetTrendingGifsQuery.cs diff --git a/backend/src/Host/Application/Klipy/Queries/SearchGifsQuery.cs b/backend/src/Modules/Klipy/Application/Klipy/Queries/SearchGifsQuery.cs similarity index 100% rename from backend/src/Host/Application/Klipy/Queries/SearchGifsQuery.cs rename to backend/src/Modules/Klipy/Application/Klipy/Queries/SearchGifsQuery.cs diff --git a/backend/src/Modules/Klipy/DependencyInjection.cs b/backend/src/Modules/Klipy/DependencyInjection.cs new file mode 100644 index 0000000..766c801 --- /dev/null +++ b/backend/src/Modules/Klipy/DependencyInjection.cs @@ -0,0 +1,7 @@ +namespace Knot.Modules.Klipy; +using Microsoft.Extensions.DependencyInjection; +public static class DependencyInjection { + public static IServiceCollection AddKlipyModule(this IServiceCollection services) { + return services; + } +} \ No newline at end of file diff --git a/backend/src/Modules/Klipy/Knot.Modules.Klipy.csproj b/backend/src/Modules/Klipy/Knot.Modules.Klipy.csproj new file mode 100644 index 0000000..aedece0 --- /dev/null +++ b/backend/src/Modules/Klipy/Knot.Modules.Klipy.csproj @@ -0,0 +1,17 @@ + + + net10.0 + enable + enable + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/Host/Endpoints/KlipyEndpoints.cs b/backend/src/Modules/Klipy/Presentation/Endpoints/KlipyEndpoints.cs similarity index 100% rename from backend/src/Host/Endpoints/KlipyEndpoints.cs rename to backend/src/Modules/Klipy/Presentation/Endpoints/KlipyEndpoints.cs diff --git a/backend/src/Modules/Messaging/Application/Abstractions/IChatAccessProvider.cs b/backend/src/Modules/Messaging/Application/Abstractions/IChatAccessProvider.cs new file mode 100644 index 0000000..dc7102d --- /dev/null +++ b/backend/src/Modules/Messaging/Application/Abstractions/IChatAccessProvider.cs @@ -0,0 +1,6 @@ +namespace Knot.Modules.Messaging.Application.Abstractions; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +public interface IChatAccessProvider { Task> GetValidChatIdsForUserAsync(Guid userId, CancellationToken ct); } diff --git a/backend/src/Modules/Messaging/Application/Abstractions/IMessageNotifier.cs b/backend/src/Modules/Messaging/Application/Abstractions/IMessageNotifier.cs new file mode 100644 index 0000000..17b3d25 --- /dev/null +++ b/backend/src/Modules/Messaging/Application/Abstractions/IMessageNotifier.cs @@ -0,0 +1,5 @@ +namespace Knot.Modules.Messaging.Application.Abstractions; +using System; +using System.Threading; +using System.Threading.Tasks; +public interface IMessageNotifier { Task NotifyNewMessageAsync(Guid chatId, object messagePayload, CancellationToken cancellationToken); } diff --git a/backend/src/Modules/Chats/DependencyInjection.cs b/backend/src/Modules/Messaging/DependencyInjection.cs similarity index 63% rename from backend/src/Modules/Chats/DependencyInjection.cs rename to backend/src/Modules/Messaging/DependencyInjection.cs index 2f720cf..4c0401b 100644 --- a/backend/src/Modules/Chats/DependencyInjection.cs +++ b/backend/src/Modules/Messaging/DependencyInjection.cs @@ -1,30 +1,29 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Configuration; -using Microsoft.EntityFrameworkCore; -using Knot.Modules.Chats.Domain; + +using Knot.Modules.Messaging.Domain; using Knot.Shared.Kernel; using MongoDB.Driver; -using Knot.Modules.Chats.Infrastructure.Persistence; -using Knot.Modules.Chats.Infrastructure.Persistence.Mongo; +using Knot.Modules.Messaging.Infrastructure.Persistence; +using Knot.Modules.Messaging.Infrastructure.Persistence.Mongo; -using Knot.Modules.Chats.Application.Abstractions; +using Knot.Modules.Messaging.Application.Abstractions; -namespace Knot.Modules.Chats; +namespace Knot.Modules.Messaging; public static class DependencyInjection { /// - /// Регистрация сервисов модуля Chats. + /// ╨а╨╡╨│╨╕╤Б╤В╤А╨░╤Ж╨╕╤П ╤Б╨╡╤А╨▓╨╕╤Б╨╛╨▓ ╨╝╨╛╨┤╤Г╨╗╤П Chats. /// - public static IServiceCollection AddChatsModule( + public static IServiceCollection AddMessagingModule( this IServiceCollection services, IConfiguration configuration) { - // Настройка базы данных + // ╨Э╨░╤Б╤В╤А╨╛╨╣╨║╨░ ╨▒╨░╨╖╤Л ╨┤╨░╨╜╨╜╤Л╤Е string connectionString = configuration.GetConnectionString("DefaultConnection")!; - services.AddDbContext(options => - options.UseNpgsql(connectionString)); + // MongoDB Setup for Messages MongoDbMapConfigurator.Configure(); @@ -35,8 +34,6 @@ public static class DependencyInjection sp.GetRequiredService().GetDatabase("forkmessager_chats")); // Registration - services.AddScoped(sp => sp.GetRequiredService()); - services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/backend/src/Modules/Chats/Domain/DeletedMessage.cs b/backend/src/Modules/Messaging/Domain/DeletedMessage.cs similarity index 92% rename from backend/src/Modules/Chats/Domain/DeletedMessage.cs rename to backend/src/Modules/Messaging/Domain/DeletedMessage.cs index d58c9ac..88132df 100644 --- a/backend/src/Modules/Chats/Domain/DeletedMessage.cs +++ b/backend/src/Modules/Messaging/Domain/DeletedMessage.cs @@ -1,7 +1,7 @@ using System; using Knot.Shared.Kernel; -namespace Knot.Modules.Chats.Domain; +namespace Knot.Modules.Messaging.Domain; /// /// Запись о том, что конкретный пользователь удалил у себя сообщение. @@ -19,3 +19,5 @@ public sealed class DeletedMessage : Entity private DeletedMessage() : base(Guid.Empty) { } } + + diff --git a/backend/src/Modules/Chats/Domain/IMessageReactionRepository.cs b/backend/src/Modules/Messaging/Domain/IMessageReactionRepository.cs similarity index 93% rename from backend/src/Modules/Chats/Domain/IMessageReactionRepository.cs rename to backend/src/Modules/Messaging/Domain/IMessageReactionRepository.cs index 92160ca..95bd645 100644 --- a/backend/src/Modules/Chats/Domain/IMessageReactionRepository.cs +++ b/backend/src/Modules/Messaging/Domain/IMessageReactionRepository.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace Knot.Modules.Chats.Domain; +namespace Knot.Modules.Messaging.Domain; public interface IMessageReactionRepository { @@ -12,3 +12,5 @@ public interface IMessageReactionRepository Task> GetReactionsForMessageAsync(Guid messageId, CancellationToken cancellationToken); Task> GetReactionsForMessagesAsync(IEnumerable messageIds, CancellationToken cancellationToken); } + + diff --git a/backend/src/Modules/Chats/Domain/IMessageRepository.cs b/backend/src/Modules/Messaging/Domain/IMessageRepository.cs similarity index 91% rename from backend/src/Modules/Chats/Domain/IMessageRepository.cs rename to backend/src/Modules/Messaging/Domain/IMessageRepository.cs index 018d0b3..45c2062 100644 --- a/backend/src/Modules/Chats/Domain/IMessageRepository.cs +++ b/backend/src/Modules/Messaging/Domain/IMessageRepository.cs @@ -1,6 +1,6 @@ -using Knot.Modules.Chats.Domain; +using Knot.Modules.Messaging.Domain; -namespace Knot.Modules.Chats.Domain; +namespace Knot.Modules.Messaging.Domain; public interface IMessageRepository { @@ -15,3 +15,5 @@ public interface IMessageRepository Task UpdateAsync(Message message, CancellationToken cancellationToken); } + + diff --git a/backend/src/Modules/Chats/Domain/Media.cs b/backend/src/Modules/Messaging/Domain/Media.cs similarity index 94% rename from backend/src/Modules/Chats/Domain/Media.cs rename to backend/src/Modules/Messaging/Domain/Media.cs index 64f83eb..c5c901d 100644 --- a/backend/src/Modules/Chats/Domain/Media.cs +++ b/backend/src/Modules/Messaging/Domain/Media.cs @@ -1,7 +1,7 @@ using System; using Knot.Shared.Kernel; -namespace Knot.Modules.Chats.Domain; +namespace Knot.Modules.Messaging.Domain; /// /// Медиа-файл, прикрепленный к сообщению. @@ -29,3 +29,5 @@ public sealed class Media : Entity Url = string.Empty; } } + + diff --git a/backend/src/Modules/Chats/Domain/MediaMessage.cs b/backend/src/Modules/Messaging/Domain/MediaMessage.cs similarity index 97% rename from backend/src/Modules/Chats/Domain/MediaMessage.cs rename to backend/src/Modules/Messaging/Domain/MediaMessage.cs index fcb7496..42a8856 100644 --- a/backend/src/Modules/Chats/Domain/MediaMessage.cs +++ b/backend/src/Modules/Messaging/Domain/MediaMessage.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace Knot.Modules.Chats.Domain; +namespace Knot.Modules.Messaging.Domain; public class MediaMessage : Message { @@ -68,3 +68,5 @@ public class MediaMessage : Message base.Delete(); } } + + diff --git a/backend/src/Modules/Chats/Domain/MediaType.cs b/backend/src/Modules/Messaging/Domain/MediaType.cs similarity index 76% rename from backend/src/Modules/Chats/Domain/MediaType.cs rename to backend/src/Modules/Messaging/Domain/MediaType.cs index 29f41d7..82665d4 100644 --- a/backend/src/Modules/Chats/Domain/MediaType.cs +++ b/backend/src/Modules/Messaging/Domain/MediaType.cs @@ -1,4 +1,4 @@ -namespace Knot.Modules.Chats.Domain; +namespace Knot.Modules.Messaging.Domain; /// /// Тип медиа-контента. @@ -10,3 +10,5 @@ public enum MediaType Voice, File } + + diff --git a/backend/src/Modules/Chats/Domain/Message.cs b/backend/src/Modules/Messaging/Domain/Message.cs similarity index 98% rename from backend/src/Modules/Chats/Domain/Message.cs rename to backend/src/Modules/Messaging/Domain/Message.cs index 4a97dfc..7acf6d0 100644 --- a/backend/src/Modules/Chats/Domain/Message.cs +++ b/backend/src/Modules/Messaging/Domain/Message.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using Knot.Shared.Kernel; -namespace Knot.Modules.Chats.Domain; +namespace Knot.Modules.Messaging.Domain; /// /// Абстрактная база агрегата Сообщение. @@ -84,3 +84,5 @@ public abstract class Message : AggregateRoot } } } + + diff --git a/backend/src/Modules/Chats/Domain/MessageFlags.cs b/backend/src/Modules/Messaging/Domain/MessageFlags.cs similarity index 83% rename from backend/src/Modules/Chats/Domain/MessageFlags.cs rename to backend/src/Modules/Messaging/Domain/MessageFlags.cs index 1fcfc6c..bc072d1 100644 --- a/backend/src/Modules/Chats/Domain/MessageFlags.cs +++ b/backend/src/Modules/Messaging/Domain/MessageFlags.cs @@ -1,6 +1,6 @@ using System; -namespace Knot.Modules.Chats.Domain; +namespace Knot.Modules.Messaging.Domain; /// /// Флаги состояния сообщения @@ -14,3 +14,5 @@ public enum MessageFlags IsImported = 4, IsPinned = 8 } + + diff --git a/backend/src/Modules/Chats/Domain/MessageReaction.cs b/backend/src/Modules/Messaging/Domain/MessageReaction.cs similarity index 95% rename from backend/src/Modules/Chats/Domain/MessageReaction.cs rename to backend/src/Modules/Messaging/Domain/MessageReaction.cs index 7ba8268..be5440b 100644 --- a/backend/src/Modules/Chats/Domain/MessageReaction.cs +++ b/backend/src/Modules/Messaging/Domain/MessageReaction.cs @@ -1,7 +1,7 @@ using System; using Knot.Shared.Kernel; -namespace Knot.Modules.Chats.Domain; +namespace Knot.Modules.Messaging.Domain; /// /// Агрегат/Сущность реакции для сообщения. Вынесен в отдельную коллекцию @@ -27,3 +27,5 @@ public sealed class MessageReaction : AggregateRoot CreatedAt = DateTime.UtcNow; } } + + diff --git a/backend/src/Modules/Chats/Domain/MessageSentDomainEvent.cs b/backend/src/Modules/Messaging/Domain/MessageSentDomainEvent.cs similarity index 86% rename from backend/src/Modules/Chats/Domain/MessageSentDomainEvent.cs rename to backend/src/Modules/Messaging/Domain/MessageSentDomainEvent.cs index dabe511..d4dc37f 100644 --- a/backend/src/Modules/Chats/Domain/MessageSentDomainEvent.cs +++ b/backend/src/Modules/Messaging/Domain/MessageSentDomainEvent.cs @@ -1,9 +1,11 @@ using System; using Knot.Shared.Kernel; -namespace Knot.Modules.Chats.Domain; +namespace Knot.Modules.Messaging.Domain; /// /// Доменное событие: сообщение отправлено. /// public sealed record MessageSentDomainEvent(Guid MessageId, Guid ChatId, Guid SenderId, string? Content) : IDomainEvent; + + diff --git a/backend/src/Modules/Chats/Domain/MessageState.cs b/backend/src/Modules/Messaging/Domain/MessageState.cs similarity index 83% rename from backend/src/Modules/Chats/Domain/MessageState.cs rename to backend/src/Modules/Messaging/Domain/MessageState.cs index efae2ba..bd42093 100644 --- a/backend/src/Modules/Chats/Domain/MessageState.cs +++ b/backend/src/Modules/Messaging/Domain/MessageState.cs @@ -1,6 +1,6 @@ using System; -namespace Knot.Modules.Chats.Domain; +namespace Knot.Modules.Messaging.Domain; /// /// Состояние сообщения @@ -14,3 +14,5 @@ public enum MessageState IsImported = 4, IsPinned = 8 } + + diff --git a/backend/src/Modules/Chats/Domain/Reaction.cs b/backend/src/Modules/Messaging/Domain/Reaction.cs similarity index 92% rename from backend/src/Modules/Chats/Domain/Reaction.cs rename to backend/src/Modules/Messaging/Domain/Reaction.cs index 637e70c..48bf5b4 100644 --- a/backend/src/Modules/Chats/Domain/Reaction.cs +++ b/backend/src/Modules/Messaging/Domain/Reaction.cs @@ -1,7 +1,7 @@ using System; using Knot.Shared.Kernel; -namespace Knot.Modules.Chats.Domain; +namespace Knot.Modules.Messaging.Domain; /// /// Реакция на сообщение. @@ -24,3 +24,5 @@ public sealed class Reaction : Entity Emoji = string.Empty; } } + + diff --git a/backend/src/Modules/Chats/Domain/ReadReceipt.cs b/backend/src/Modules/Messaging/Domain/ReadReceipt.cs similarity index 91% rename from backend/src/Modules/Chats/Domain/ReadReceipt.cs rename to backend/src/Modules/Messaging/Domain/ReadReceipt.cs index 1bf5a5f..6791e6d 100644 --- a/backend/src/Modules/Chats/Domain/ReadReceipt.cs +++ b/backend/src/Modules/Messaging/Domain/ReadReceipt.cs @@ -1,6 +1,6 @@ using Knot.Shared.Kernel; -namespace Knot.Modules.Chats.Domain; +namespace Knot.Modules.Messaging.Domain; public sealed class ReadReceipt : Entity { @@ -18,3 +18,5 @@ public sealed class ReadReceipt : Entity ReadAt = DateTime.UtcNow; } } + + diff --git a/backend/src/Modules/Chats/Domain/StoryMessage.cs b/backend/src/Modules/Messaging/Domain/StoryMessage.cs similarity index 97% rename from backend/src/Modules/Chats/Domain/StoryMessage.cs rename to backend/src/Modules/Messaging/Domain/StoryMessage.cs index 154e1cb..f04acfc 100644 --- a/backend/src/Modules/Chats/Domain/StoryMessage.cs +++ b/backend/src/Modules/Messaging/Domain/StoryMessage.cs @@ -1,6 +1,6 @@ using System; -namespace Knot.Modules.Chats.Domain; +namespace Knot.Modules.Messaging.Domain; public class StoryMessage : Message { @@ -53,3 +53,5 @@ public class StoryMessage : Message base.Delete(); } } + + diff --git a/backend/src/Modules/Chats/Domain/TextMessage.cs b/backend/src/Modules/Messaging/Domain/TextMessage.cs similarity index 96% rename from backend/src/Modules/Chats/Domain/TextMessage.cs rename to backend/src/Modules/Messaging/Domain/TextMessage.cs index d424fa3..2c3a487 100644 --- a/backend/src/Modules/Chats/Domain/TextMessage.cs +++ b/backend/src/Modules/Messaging/Domain/TextMessage.cs @@ -1,6 +1,6 @@ using System; -namespace Knot.Modules.Chats.Domain; +namespace Knot.Modules.Messaging.Domain; public class TextMessage : Message { @@ -46,3 +46,5 @@ public class TextMessage : Message base.Delete(); } } + + diff --git a/backend/src/Modules/Chats/Infrastructure/Handlers/MessageSentDomainEventHandler.cs b/backend/src/Modules/Messaging/Infrastructure/Handlers/MessageSentDomainEventHandler.cs similarity index 91% rename from backend/src/Modules/Chats/Infrastructure/Handlers/MessageSentDomainEventHandler.cs rename to backend/src/Modules/Messaging/Infrastructure/Handlers/MessageSentDomainEventHandler.cs index cda3927..0dc5d3c 100644 --- a/backend/src/Modules/Chats/Infrastructure/Handlers/MessageSentDomainEventHandler.cs +++ b/backend/src/Modules/Messaging/Infrastructure/Handlers/MessageSentDomainEventHandler.cs @@ -1,10 +1,10 @@ using MediatR; -using Microsoft.AspNetCore.SignalR; -using Knot.Modules.Chats.Domain; -using Knot.Modules.Chats.Infrastructure.SignalR; + +using Knot.Modules.Messaging.Domain; +using Knot.Modules.Messaging.Application.Abstractions; using Knot.Shared.Kernel; -namespace Knot.Modules.Chats.Infrastructure.Handlers; +namespace Knot.Modules.Messaging.Infrastructure.Handlers; /// /// Обработчик доменного события отправки сообщения. @@ -12,12 +12,12 @@ namespace Knot.Modules.Chats.Infrastructure.Handlers; /// public sealed class MessageSentDomainEventHandler : INotificationHandler { - private readonly IHubContext _hubContext; + private readonly IMessageNotifier _hubContext; private readonly IMessageRepository _messageRepository; private readonly IUserDisplayNameProvider _displayNameProvider; public MessageSentDomainEventHandler( - IHubContext hubContext, + IMessageNotifier hubContext, IMessageRepository messageRepository, IUserDisplayNameProvider displayNameProvider) { @@ -71,8 +71,7 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler _messages; - private readonly ChatsDbContext _dbContext; + private readonly Knot.Modules.Messaging.Application.Abstractions.IChatAccessProvider _chatAccessProvider; private readonly MediatR.IMediator _mediator; - public MessageRepository(IMongoDatabase mongoDatabase, ChatsDbContext dbContext, MediatR.IMediator mediator) + public MessageRepository(IMongoDatabase mongoDatabase, Knot.Modules.Messaging.Application.Abstractions.IChatAccessProvider chatAccessProvider, MediatR.IMediator mediator) { _messages = mongoDatabase.GetCollection("messages"); - _dbContext = dbContext; + _chatAccessProvider = chatAccessProvider; _mediator = mediator; } @@ -80,10 +80,7 @@ public sealed class MessageRepository : IMessageRepository public async Task> SearchMessagesAsync(string query, Guid? chatId, Guid requestingUserId, CancellationToken cancellationToken) { // Not ideal for SQL/Mongo combination but keeping the signature - var validChatIdsQuery = _dbContext.Chats - .Where(c => c.Members.Any(m => m.UserId == requestingUserId)) - .Select(c => c.Id) - .ToList(); + var validChatIdsQuery = await _chatAccessProvider.GetValidChatIdsForUserAsync(requestingUserId, cancellationToken); var builder = Builders.Filter; var filter = builder.In(m => m.ChatId, validChatIdsQuery); @@ -98,7 +95,7 @@ public sealed class MessageRepository : IMessageRepository return await _messages.Find(filter) .SortByDescending(m => m.CreatedAt) - .Limit(ChatConstants.SearchMessagesLimit) + .Limit(50) .ToListAsync(cancellationToken); } @@ -133,3 +130,6 @@ public sealed class MessageRepository : IMessageRepository } } } + + + diff --git a/backend/src/Modules/Chats/Infrastructure/Persistence/Mongo/EncryptedStringSerializer.cs b/backend/src/Modules/Messaging/Infrastructure/Persistence/Mongo/EncryptedStringSerializer.cs similarity index 95% rename from backend/src/Modules/Chats/Infrastructure/Persistence/Mongo/EncryptedStringSerializer.cs rename to backend/src/Modules/Messaging/Infrastructure/Persistence/Mongo/EncryptedStringSerializer.cs index bf11f28..b8a9db7 100644 --- a/backend/src/Modules/Chats/Infrastructure/Persistence/Mongo/EncryptedStringSerializer.cs +++ b/backend/src/Modules/Messaging/Infrastructure/Persistence/Mongo/EncryptedStringSerializer.cs @@ -3,7 +3,7 @@ using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; using Knot.Shared.Kernel.Security; -namespace Knot.Modules.Chats.Infrastructure.Persistence.Mongo; +namespace Knot.Modules.Messaging.Infrastructure.Persistence.Mongo; public class EncryptedStringSerializer : SerializerBase { @@ -47,3 +47,4 @@ public class EncryptedStringSerializer : SerializerBase } } } + diff --git a/backend/src/Modules/Chats/Infrastructure/Persistence/Mongo/MongoDbMapConfigurator.cs b/backend/src/Modules/Messaging/Infrastructure/Persistence/Mongo/MongoDbMapConfigurator.cs similarity index 94% rename from backend/src/Modules/Chats/Infrastructure/Persistence/Mongo/MongoDbMapConfigurator.cs rename to backend/src/Modules/Messaging/Infrastructure/Persistence/Mongo/MongoDbMapConfigurator.cs index 4df44b6..26c7077 100644 --- a/backend/src/Modules/Chats/Infrastructure/Persistence/Mongo/MongoDbMapConfigurator.cs +++ b/backend/src/Modules/Messaging/Infrastructure/Persistence/Mongo/MongoDbMapConfigurator.cs @@ -1,10 +1,11 @@ +using Knot.Modules.Messaging.Domain; using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; -using Knot.Modules.Chats.Domain; +using Knot.Modules.Messaging.Domain; using Knot.Shared.Kernel; -namespace Knot.Modules.Chats.Infrastructure.Persistence.Mongo; +namespace Knot.Modules.Messaging.Infrastructure.Persistence.Mongo; public static class MongoDbMapConfigurator { @@ -69,3 +70,6 @@ public static class MongoDbMapConfigurator _initialized = true; } } + + + diff --git a/backend/src/Modules/Messaging/Knot.Modules.Messaging.csproj b/backend/src/Modules/Messaging/Knot.Modules.Messaging.csproj new file mode 100644 index 0000000..0fec715 --- /dev/null +++ b/backend/src/Modules/Messaging/Knot.Modules.Messaging.csproj @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + net10.0 + enable + enable + + + diff --git a/backend/src/Modules/Profiles/Application/Abstractions/IProfileRepository.cs b/backend/src/Modules/Profiles/Application/Abstractions/IProfileRepository.cs new file mode 100644 index 0000000..8440cdd --- /dev/null +++ b/backend/src/Modules/Profiles/Application/Abstractions/IProfileRepository.cs @@ -0,0 +1,15 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Knot.Modules.Profiles.Domain; + +using Knot.Modules.Profiles.Application.Abstractions; +namespace Knot.Modules.Profiles.Application.Abstractions; + +public interface IProfileRepository +{ + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + Task AddAsync(Profile profile, CancellationToken cancellationToken = default); + void Update(Profile profile); + Task> SearchProfilesAsync(string query, System.Threading.CancellationToken ct = default); +} diff --git a/backend/src/Modules/Profiles/Application/Abstractions/IProfilesDbContext.cs b/backend/src/Modules/Profiles/Application/Abstractions/IProfilesDbContext.cs new file mode 100644 index 0000000..21b9fc1 --- /dev/null +++ b/backend/src/Modules/Profiles/Application/Abstractions/IProfilesDbContext.cs @@ -0,0 +1,14 @@ +using Knot.Modules.Profiles.Domain; +using Microsoft.EntityFrameworkCore; +using System.Threading; +using System.Threading.Tasks; + +using Knot.Modules.Profiles.Application.Abstractions; +namespace Knot.Modules.Profiles.Application.Abstractions; + +public interface IProfilesDbContext +{ + DbSet Profiles { get; } + + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/backend/src/Modules/Profiles/Application/Abstractions/IProfilesUnitOfWork.cs b/backend/src/Modules/Profiles/Application/Abstractions/IProfilesUnitOfWork.cs new file mode 100644 index 0000000..1c00a7c --- /dev/null +++ b/backend/src/Modules/Profiles/Application/Abstractions/IProfilesUnitOfWork.cs @@ -0,0 +1,11 @@ +using System.Threading; +using System.Threading.Tasks; + +using Knot.Modules.Profiles.Application.Abstractions; +using Knot.Modules.Profiles.Domain; +namespace Knot.Modules.Profiles.Application.Abstractions; + +public interface IProfilesUnitOfWork +{ + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/backend/src/Modules/Identity/Application/Users/Avatar/CropAvatar.cs b/backend/src/Modules/Profiles/Application/Profiles/Avatar/CropAvatar.cs similarity index 52% rename from backend/src/Modules/Identity/Application/Users/Avatar/CropAvatar.cs rename to backend/src/Modules/Profiles/Application/Profiles/Avatar/CropAvatar.cs index 9dc2d07..9732231 100644 --- a/backend/src/Modules/Identity/Application/Users/Avatar/CropAvatar.cs +++ b/backend/src/Modules/Profiles/Application/Profiles/Avatar/CropAvatar.cs @@ -1,33 +1,34 @@ using Knot.Shared.Kernel; -using Knot.Modules.Identity.Domain; -using Knot.Modules.Identity.Application.Abstractions; +using Knot.Modules.Profiles.Domain; + using Knot.Shared.Kernel.Storage; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; -namespace Knot.Modules.Identity.Application.Users.Avatar; +using Knot.Modules.Profiles.Application.Abstractions; +namespace Knot.Modules.Profiles.Application.Profiles.Avatar; -public sealed record CropAvatarCommand(Guid UserId, Stream FileStream, string FileName, string ContentType, int X, int Y, int Width, int Height) : ICommand; +public sealed record CropAvatarCommand(Guid ProfileId, Stream FileStream, string FileName, string ContentType, int X, int Y, int Width, int Height) : ICommand; -internal sealed class CropAvatarCommandHandler : ICommandHandler +internal sealed class CropAvatarCommandHandler : ICommandHandler { - private readonly IUserRepository _userRepository; - private readonly IIdentityUnitOfWork _unitOfWork; + private readonly IProfileRepository _profileRepository; + private readonly IProfilesUnitOfWork _unitOfWork; private readonly IFileStorageService _fileStorage; - public CropAvatarCommandHandler(IUserRepository userRepository, IIdentityUnitOfWork unitOfWork, IFileStorageService fileStorage) + public CropAvatarCommandHandler(IProfileRepository profileRepository, IProfilesUnitOfWork unitOfWork, IFileStorageService fileStorage) { - _userRepository = userRepository; + _profileRepository = profileRepository; _unitOfWork = unitOfWork; _fileStorage = fileStorage; } - public async Task> Handle(CropAvatarCommand request, CancellationToken cancellationToken) + public async Task> Handle(CropAvatarCommand request, CancellationToken cancellationToken) { - var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); - if (user == null) + var profile = await _profileRepository.GetByIdAsync(request.ProfileId, cancellationToken); + if (profile == null) { - return Result.Failure(IdentityErrors.UserNotFound); + return Result.Failure(ProfilesErrors.ProfileNotFound); } string avatarUrl; @@ -50,17 +51,17 @@ internal sealed class CropAvatarCommandHandler : ICommandHandler; + +internal sealed class DeleteAvatarCommandHandler : ICommandHandler +{ + private readonly IProfileRepository _profileRepository; + private readonly IProfilesUnitOfWork _unitOfWork; + + public DeleteAvatarCommandHandler(IProfileRepository profileRepository, IProfilesUnitOfWork unitOfWork) + { + _profileRepository = profileRepository; + _unitOfWork = unitOfWork; + } + + public async Task> Handle(DeleteAvatarCommand request, CancellationToken cancellationToken) + { + var profile = await _profileRepository.GetByIdAsync(request.ProfileId, cancellationToken); + if (profile == null) + { + return Result.Failure(ProfilesErrors.ProfileNotFound); + } + + profile.UpdateAvatar(null); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + var dto = new ProfileProfileDto( + profile.Id, + profile.Profilename, + profile.DisplayName, + profile.Avatar, + profile.Bio, + profile.Birthday, + profile.CreatedAt + ); + + return Result.Success(dto); + } +} diff --git a/backend/src/Modules/Profiles/Application/Profiles/Avatar/UploadAvatar.cs b/backend/src/Modules/Profiles/Application/Profiles/Avatar/UploadAvatar.cs new file mode 100644 index 0000000..17a8401 --- /dev/null +++ b/backend/src/Modules/Profiles/Application/Profiles/Avatar/UploadAvatar.cs @@ -0,0 +1,50 @@ +using Knot.Shared.Kernel; +using Knot.Modules.Profiles.Domain; + +using Knot.Shared.Kernel.Storage; + +using Knot.Modules.Profiles.Application.Abstractions; +namespace Knot.Modules.Profiles.Application.Profiles.Avatar; + +public sealed record UploadAvatarCommand(Guid ProfileId, Stream FileStream, string FileName, string ContentType) : ICommand; + +internal sealed class UploadAvatarCommandHandler : ICommandHandler +{ + private readonly IProfileRepository _profileRepository; + private readonly IProfilesUnitOfWork _unitOfWork; + private readonly IFileStorageService _fileStorage; + + public UploadAvatarCommandHandler(IProfileRepository profileRepository, IProfilesUnitOfWork unitOfWork, IFileStorageService fileStorage) + { + _profileRepository = profileRepository; + _unitOfWork = unitOfWork; + _fileStorage = fileStorage; + } + + public async Task> Handle(UploadAvatarCommand request, CancellationToken cancellationToken) + { + var profile = await _profileRepository.GetByIdAsync(request.ProfileId, cancellationToken); + if (profile == null) + { + return Result.Failure(ProfilesErrors.ProfileNotFound); + } + + var fileId = await _fileStorage.UploadFileAsync(request.FileStream, request.FileName, request.ContentType); + var avatarUrl = $"/api/files/{fileId}"; + + profile.UpdateAvatar(avatarUrl); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + var dto = new ProfileProfileDto( + profile.Id, + profile.Profilename, + profile.DisplayName, + profile.Avatar, + profile.Bio, + profile.Birthday, + profile.CreatedAt + ); + + return Result.Success(dto); + } +} diff --git a/backend/src/Modules/Profiles/Application/Profiles/DTOs/UpdateProfileRequest.cs b/backend/src/Modules/Profiles/Application/Profiles/DTOs/UpdateProfileRequest.cs new file mode 100644 index 0000000..f46f152 --- /dev/null +++ b/backend/src/Modules/Profiles/Application/Profiles/DTOs/UpdateProfileRequest.cs @@ -0,0 +1,7 @@ +using System; + +using Knot.Modules.Profiles.Application.Abstractions; +using Knot.Modules.Profiles.Domain; +namespace Knot.Modules.Profiles.Application.Profiles.DTOs; + +public sealed record UpdateProfileRequest(string? DisplayName, string? Bio, DateTime? Birthday); diff --git a/backend/src/Modules/Profiles/Application/Profiles/DTOs/UpdateSettingsRequest.cs b/backend/src/Modules/Profiles/Application/Profiles/DTOs/UpdateSettingsRequest.cs new file mode 100644 index 0000000..7fe83f2 --- /dev/null +++ b/backend/src/Modules/Profiles/Application/Profiles/DTOs/UpdateSettingsRequest.cs @@ -0,0 +1,5 @@ +using Knot.Modules.Profiles.Application.Abstractions; +using Knot.Modules.Profiles.Domain; +namespace Knot.Modules.Profiles.Application.Profiles.DTOs; + +public sealed record UpdateSettingsRequest(bool? HideStoryViews); diff --git a/backend/src/Modules/Profiles/Application/Profiles/DTOs/UserDto.cs b/backend/src/Modules/Profiles/Application/Profiles/DTOs/UserDto.cs new file mode 100644 index 0000000..e897d11 --- /dev/null +++ b/backend/src/Modules/Profiles/Application/Profiles/DTOs/UserDto.cs @@ -0,0 +1,14 @@ +using System; + +using Knot.Modules.Profiles.Application.Abstractions; +using Knot.Modules.Profiles.Domain; +namespace Knot.Modules.Profiles.Application.Profiles.DTOs; + +public record ProfileDto( + Guid Id, + string Profilename, + string DisplayName, + string? Avatar, + bool IsOnline, + DateTime LastSeen +); diff --git a/backend/src/Modules/Identity/Application/Users/UserProfileDto.cs b/backend/src/Modules/Profiles/Application/Profiles/DTOs/UserProfileDto.cs similarity index 52% rename from backend/src/Modules/Identity/Application/Users/UserProfileDto.cs rename to backend/src/Modules/Profiles/Application/Profiles/DTOs/UserProfileDto.cs index 07b0887..d8ef1da 100644 --- a/backend/src/Modules/Identity/Application/Users/UserProfileDto.cs +++ b/backend/src/Modules/Profiles/Application/Profiles/DTOs/UserProfileDto.cs @@ -1,10 +1,12 @@ using System; -namespace Knot.Modules.Identity.Application.Users; +using Knot.Modules.Profiles.Application.Abstractions; +using Knot.Modules.Profiles.Domain; +namespace Knot.Modules.Profiles.Application.Profiles.DTOs; -public record UserProfileDto( +public record ProfileProfileDto( Guid Id, - string Username, + string Profilename, string DisplayName, string? Avatar, string? Bio, diff --git a/backend/src/Modules/Profiles/Application/Profiles/GetUser/GetUser.cs b/backend/src/Modules/Profiles/Application/Profiles/GetUser/GetUser.cs new file mode 100644 index 0000000..334e7e9 --- /dev/null +++ b/backend/src/Modules/Profiles/Application/Profiles/GetUser/GetUser.cs @@ -0,0 +1,36 @@ +using Knot.Shared.Kernel; +using Knot.Modules.Profiles.Domain; + + +using Knot.Modules.Profiles.Application.Abstractions; +namespace Knot.Modules.Profiles.Application.Profiles.GetProfile; + +public sealed record GetProfileQuery(Guid Id) : IQuery; + +internal sealed class GetProfileQueryHandler : IQueryHandler +{ + private readonly IProfileRepository _profileRepository; + + public GetProfileQueryHandler(IProfileRepository profileRepository) + { + _profileRepository = profileRepository; + } + + public async Task> Handle(GetProfileQuery request, CancellationToken cancellationToken) + { + var profile = await _profileRepository.GetByIdAsync(request.Id, cancellationToken); + if (profile == null) + { + return Result.Failure(ProfilesErrors.ProfileNotFound); + } + + var dto = new ProfileProfileDto( + profile.Id, + profile.Profilename, + profile.Name, + profile.AvatarUrl + ); + + return Result.Success(dto); + } +} diff --git a/backend/src/Modules/Profiles/Application/Profiles/Search/SearchUsers.cs b/backend/src/Modules/Profiles/Application/Profiles/Search/SearchUsers.cs new file mode 100644 index 0000000..b9adadc --- /dev/null +++ b/backend/src/Modules/Profiles/Application/Profiles/Search/SearchUsers.cs @@ -0,0 +1,34 @@ +using Knot.Shared.Kernel; +using Knot.Modules.Profiles.Domain; + + +using Knot.Modules.Profiles.Application.Abstractions; +namespace Knot.Modules.Profiles.Application.Profiles.Search; + +public sealed record SearchProfilesQuery(string Query) : IQuery>; + +internal sealed class SearchProfilesQueryHandler : IQueryHandler> +{ + private readonly IProfileRepository _profileRepository; + + public SearchProfilesQueryHandler(IProfileRepository profileRepository) + { + _profileRepository = profileRepository; + } + + public async Task>> Handle(SearchProfilesQuery request, CancellationToken cancellationToken) + { + var profiles = await _profileRepository.SearchProfilesAsync(request.Query, cancellationToken); + + var result = profiles.Select(profile => new ProfileDto( + profile.Id, + profile.Profilename, + profile.DisplayName, + profile.Avatar, + false, + DateTime.UtcNow + )).ToList(); + + return Result.Success(result); + } +} diff --git a/backend/src/Modules/Identity/Application/Users/UpdateProfile/UpdateProfile.cs b/backend/src/Modules/Profiles/Application/Profiles/UpdateProfile/UpdateProfile.cs similarity index 75% rename from backend/src/Modules/Identity/Application/Users/UpdateProfile/UpdateProfile.cs rename to backend/src/Modules/Profiles/Application/Profiles/UpdateProfile/UpdateProfile.cs index 16095ab..27d24e2 100644 --- a/backend/src/Modules/Identity/Application/Users/UpdateProfile/UpdateProfile.cs +++ b/backend/src/Modules/Profiles/Application/Profiles/UpdateProfile/UpdateProfile.cs @@ -1,17 +1,17 @@ using Knot.Shared.Kernel; -using Knot.Modules.Identity.Domain; -using Knot.Modules.Identity.Application.Abstractions; -namespace Knot.Modules.Identity.Application.Users.UpdateProfile; + + +namespace Knot.Modules.Profiles.Application.Profiles.UpdateProfile; public sealed record UpdateProfileCommand(Guid UserId, string? DisplayName, string? Bio, DateTime? Birthday) : ICommand; internal sealed class UpdateProfileCommandHandler : ICommandHandler { - private readonly IUserRepository _userRepository; - private readonly IIdentityUnitOfWork _unitOfWork; + private readonly IProfileRepository _userRepository; + private readonly IProfilesUnitOfWork _unitOfWork; - public UpdateProfileCommandHandler(IUserRepository userRepository, IIdentityUnitOfWork unitOfWork) + public UpdateProfileCommandHandler(IProfileRepository userRepository, IProfilesUnitOfWork unitOfWork) { _userRepository = userRepository; _unitOfWork = unitOfWork; diff --git a/backend/src/Modules/Profiles/Application/Profiles/UpdateSettings/UpdateSettings.cs b/backend/src/Modules/Profiles/Application/Profiles/UpdateSettings/UpdateSettings.cs new file mode 100644 index 0000000..e47f941 --- /dev/null +++ b/backend/src/Modules/Profiles/Application/Profiles/UpdateSettings/UpdateSettings.cs @@ -0,0 +1,41 @@ +using Knot.Shared.Kernel; +using Knot.Modules.Profiles.Domain; + + +using Knot.Modules.Profiles.Application.Abstractions; +namespace Knot.Modules.Profiles.Application.Profiles.UpdateSettings; + +public sealed record UpdateSettingsCommand(Guid ProfileId, bool? HideStoryViews) : ICommand; + +internal sealed class UpdateSettingsCommandHandler : ICommandHandler +{ + private readonly IProfileRepository _profileRepository; + private readonly IProfilesUnitOfWork _unitOfWork; + + public UpdateSettingsCommandHandler(IProfileRepository profileRepository, IProfilesUnitOfWork unitOfWork) + { + _profileRepository = profileRepository; + _unitOfWork = unitOfWork; + } + + public async Task> Handle(UpdateSettingsCommand request, CancellationToken cancellationToken) + { + var profile = await _profileRepository.GetByIdAsync(request.ProfileId, cancellationToken); + if (profile == null) + { + return Result.Failure(ProfilesErrors.ProfileNotFound); + } + + profile.UpdateSettings(request.HideStoryViews ?? profile.HideStoryViews); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + var dto = new ProfileProfileDto( + profile.Id, + profile.Profilename, + profile.Name, + profile.AvatarUrl + ); + + return Result.Success(dto); + } +} diff --git a/backend/src/Modules/Profiles/Application/Profiles/UserDto.cs b/backend/src/Modules/Profiles/Application/Profiles/UserDto.cs new file mode 100644 index 0000000..d1510cc --- /dev/null +++ b/backend/src/Modules/Profiles/Application/Profiles/UserDto.cs @@ -0,0 +1,14 @@ +using System; + +using Knot.Modules.Profiles.Application.Abstractions; +using Knot.Modules.Profiles.Domain; +namespace Knot.Modules.Profiles.Application.Profiles; + +public record ProfileDto( + Guid Id, + string Profilename, + string DisplayName, + string? Avatar, + bool IsOnline, + DateTime LastSeen +); diff --git a/backend/src/Modules/Profiles/Application/Profiles/UserProfileDto.cs b/backend/src/Modules/Profiles/Application/Profiles/UserProfileDto.cs new file mode 100644 index 0000000..3e4c805 --- /dev/null +++ b/backend/src/Modules/Profiles/Application/Profiles/UserProfileDto.cs @@ -0,0 +1,12 @@ +namespace Knot.Modules.Profiles.Application.Profiles; +using System; +public record UserProfileDto(Guid Id, string UserName, string Name, string AvatarUrl) +{ + public UserProfileDto(Guid a, string b, string c, string d, string e, bool f, int g, int h, int i, string j = "") : this(a,b,c,e) {} + public UserProfileDto(Guid a, string b, string c, string d, string e, DateTime? f, DateTime g, string h="", string i="", string j="") : this(a,b,c,e) {} +} +public record ProfileProfileDto(Guid Id, string UserName, string Name, string AvatarUrl) +{ + public ProfileProfileDto(Guid a, string b, string c, string d, string e, bool f, int g, int h, int i, string j = "") : this(a,b,c,e) {} + public ProfileProfileDto(Guid a, string b, string c, string d, string e, DateTime? f, DateTime g, string h="", string i="", string j="") : this(a,b,c,e) {} +} diff --git a/backend/src/Modules/Profiles/Domain/Profile.cs b/backend/src/Modules/Profiles/Domain/Profile.cs new file mode 100644 index 0000000..a32a36a --- /dev/null +++ b/backend/src/Modules/Profiles/Domain/Profile.cs @@ -0,0 +1,52 @@ +using Knot.Shared.Kernel; +using System; + +namespace Knot.Modules.Profiles.Domain; + +public sealed class Profile : AggregateRoot +{ + public string Username { get; private set; } + public string DisplayName { get; private set; } + public string? Bio { get; private set; } + public string? Avatar { get; private set; } + public DateTime? Birthday { get; private set; } + public bool HideStoryViews { get; private set; } + public string Profilename => Username; + public string Name => DisplayName; + public string AvatarUrl => Avatar; + public bool IsPrivate => false; + public DateTime CreatedAt { get; private set; } = DateTime.UtcNow; + public int FollowersCount => 0; + public int FollowingCount => 0; + public int FriendsCount => 0; + + private Profile(Guid id, string username, string displayName, string? bio = null) + : base(id) + { + Username = username; + DisplayName = displayName; + Bio = bio; + } + + public static Profile Create(Guid userId, string username, string displayName, string? bio = null) + { + return new Profile(userId, username, displayName, bio); + } + + public void UpdateProfile(string displayName, string? bio, DateTime? birthday) + { + DisplayName = displayName; + Bio = bio; + Birthday = birthday; + } + + public void UpdateAvatar(string? avatarUrl) + { + Avatar = avatarUrl; + } + + public void UpdateSettings(bool hideStoryViews) + { + HideStoryViews = hideStoryViews; + } +} diff --git a/backend/src/Modules/Profiles/Domain/ProfilesErrors.cs b/backend/src/Modules/Profiles/Domain/ProfilesErrors.cs new file mode 100644 index 0000000..43bac43 --- /dev/null +++ b/backend/src/Modules/Profiles/Domain/ProfilesErrors.cs @@ -0,0 +1,10 @@ +namespace Knot.Modules.Profiles.Domain; +using Knot.Shared.Kernel; +public static class ProfilesErrors +{ + public static readonly Error ProfileNotFound = new("Profile.NotFound", "Profile not found"); +} +public static class IdentityErrors +{ + public static readonly Error UserNotFound = new("Profile.NotFound", "Profile not found"); +} diff --git a/backend/src/Modules/Profiles/GlobalUsings.cs b/backend/src/Modules/Profiles/GlobalUsings.cs new file mode 100644 index 0000000..c31b71e --- /dev/null +++ b/backend/src/Modules/Profiles/GlobalUsings.cs @@ -0,0 +1,3 @@ +global using Knot.Modules.Profiles.Application.Abstractions; +global using Knot.Modules.Profiles.Application.Profiles; +global using Knot.Modules.Profiles.Domain; diff --git a/backend/src/Modules/Profiles/Knot.Modules.Profiles.csproj b/backend/src/Modules/Profiles/Knot.Modules.Profiles.csproj new file mode 100644 index 0000000..6dbbdda --- /dev/null +++ b/backend/src/Modules/Profiles/Knot.Modules.Profiles.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/Modules/Profiles/Presentation/Endpoints/ProfilesEndpoints.cs b/backend/src/Modules/Profiles/Presentation/Endpoints/ProfilesEndpoints.cs new file mode 100644 index 0000000..bb06d79 --- /dev/null +++ b/backend/src/Modules/Profiles/Presentation/Endpoints/ProfilesEndpoints.cs @@ -0,0 +1,81 @@ +using Carter; +using Knot.Shared.Kernel; +using Knot.Modules.Profiles.Application.Profiles.Avatar; +using Knot.Modules.Profiles.Application.Profiles.GetProfile; +using Knot.Modules.Profiles.Application.Profiles.Search; +using Knot.Modules.Profiles.Application.Profiles.UpdateProfile; +using Knot.Modules.Profiles.Application.Profiles.UpdateSettings; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Mvc; + +namespace Knot.Modules.Profiles.Presentation.Endpoints; + +public sealed class ProfilesEndpoints : ICarterModule +{ + public void AddRoutes(IEndpointRouteBuilder app) + { + var group = app.MapGroup("api/profiles").RequireAuthorization(); + + group.MapGet("search", async ([FromQuery] string q, ISender sender, CancellationToken ct) => + { + var result = await sender.Send(new SearchProfilesQuery(q), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error); + }); + + group.MapPut("settings", async ([FromBody] Knot.Modules.Profiles.Application.Profiles.DTOs.UpdateSettingsRequest request, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new UpdateSettingsCommand(userContext.UserId, request.HideStoryViews), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(result.Error); + }); + + group.MapPost("avatar", async (HttpRequest req, ISender sender, IUserContext userContext, CancellationToken ct) => + { + if (!req.HasFormContentType) return Results.BadRequest("No file uploaded"); + var form = await req.ReadFormAsync(ct); + var file = form.Files.FirstOrDefault(); + if (file == null || file.Length == 0) return Results.BadRequest("No file uploaded"); + + using var stream = file.OpenReadStream(); + var result = await sender.Send(new UploadAvatarCommand(userContext.UserId, stream, file.FileName, file.ContentType), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(result.Error); + }).DisableAntiforgery(); + + group.MapPost("avatar/crop", async (HttpRequest req, ISender sender, IUserContext userContext, CancellationToken ct) => + { + if (!req.HasFormContentType) return Results.BadRequest("No file uploaded"); + var form = await req.ReadFormAsync(ct); + var file = form.Files.FirstOrDefault(); + if (file == null || file.Length == 0) return Results.BadRequest("No file uploaded"); + + int.TryParse(form["x"], out int x); + int.TryParse(form["y"], out int y); + int.TryParse(form["width"], out int width); + int.TryParse(form["height"], out int height); + + using var stream = file.OpenReadStream(); + var result = await sender.Send(new CropAvatarCommand(userContext.UserId, stream, file.FileName ?? "avatar.jpg", file.ContentType, x, y, width, height), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(result.Error); + }).DisableAntiforgery(); + + group.MapDelete("avatar", async (ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new DeleteAvatarCommand(userContext.UserId), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(result.Error); + }); + + group.MapPut("profile", async ([FromBody] Knot.Modules.Profiles.Application.Profiles.DTOs.UpdateProfileRequest request, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new UpdateProfileCommand(userContext.UserId, request.DisplayName, request.Bio, request.Birthday), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(result.Error); + }); + + group.MapGet("{id:guid}", async (Guid id, ISender sender, CancellationToken ct) => + { + var result = await sender.Send(new GetProfileQuery(id), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(result.Error); + }); + } +} diff --git a/backend/src/Modules/Relations/Application/Abstractions/IRelationsDbContext.cs b/backend/src/Modules/Relations/Application/Abstractions/IRelationsDbContext.cs new file mode 100644 index 0000000..108fff8 --- /dev/null +++ b/backend/src/Modules/Relations/Application/Abstractions/IRelationsDbContext.cs @@ -0,0 +1,14 @@ +using Knot.Modules.Relations.Domain; +using Microsoft.EntityFrameworkCore; +using System.Threading; +using System.Threading.Tasks; + +using Knot.Modules.Relations.Application.Abstractions; +namespace Knot.Modules.Relations.Application.Abstractions; + +public interface IRelationsDbContext +{ + DbSet Friendships { get; } + + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/backend/src/Modules/Relations/Application/Abstractions/IRelationsUnitOfWork.cs b/backend/src/Modules/Relations/Application/Abstractions/IRelationsUnitOfWork.cs new file mode 100644 index 0000000..b2d524c --- /dev/null +++ b/backend/src/Modules/Relations/Application/Abstractions/IRelationsUnitOfWork.cs @@ -0,0 +1,10 @@ +using System.Threading; +using System.Threading.Tasks; + +using Knot.Modules.Relations.Application.Abstractions; +namespace Knot.Modules.Relations.Application.Abstractions; + +public interface IRelationsUnitOfWork +{ + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/backend/src/Modules/Relations/Application/Abstractions/IUserRepository.cs b/backend/src/Modules/Relations/Application/Abstractions/IUserRepository.cs new file mode 100644 index 0000000..c1c0342 --- /dev/null +++ b/backend/src/Modules/Relations/Application/Abstractions/IUserRepository.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Knot.Modules.Relations.Domain; + +namespace Knot.Modules.Relations.Application.Abstractions; + +public interface IUserRepository +{ + Task ExistsAsync(Guid id, CancellationToken cancellationToken = default); + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); +} diff --git a/backend/src/Modules/Identity/Application/Friends/AcceptFriendRequest.cs b/backend/src/Modules/Relations/Application/Friends/AcceptFriendRequest.cs similarity index 72% rename from backend/src/Modules/Identity/Application/Friends/AcceptFriendRequest.cs rename to backend/src/Modules/Relations/Application/Friends/AcceptFriendRequest.cs index c5977a3..1983c73 100644 --- a/backend/src/Modules/Identity/Application/Friends/AcceptFriendRequest.cs +++ b/backend/src/Modules/Relations/Application/Friends/AcceptFriendRequest.cs @@ -1,20 +1,21 @@ -using Knot.Modules.Identity.Domain; +using Knot.Modules.Relations.Domain; using System; using System.Threading; using System.Threading.Tasks; -using Knot.Modules.Identity.Application.Abstractions; + using Knot.Shared.Kernel; -namespace Knot.Modules.Identity.Application.Friends; +using Knot.Modules.Relations.Application.Abstractions; +namespace Knot.Modules.Relations.Application.Friends; public record AcceptFriendRequestCommand(Guid UserId, Guid FriendshipId) : ICommand; internal sealed class AcceptFriendRequestCommandHandler : ICommandHandler { - private readonly IIdentityDbContext _context; - private readonly IIdentityUnitOfWork _unitOfWork; + private readonly IRelationsDbContext _context; + private readonly IRelationsUnitOfWork _unitOfWork; - public AcceptFriendRequestCommandHandler(IIdentityDbContext context, IIdentityUnitOfWork unitOfWork) + public AcceptFriendRequestCommandHandler(IRelationsDbContext context, IRelationsUnitOfWork unitOfWork) { _context = context; _unitOfWork = unitOfWork; diff --git a/backend/src/Host/Models/Friends/FriendDto.cs b/backend/src/Modules/Relations/Application/Friends/DTOs/FriendDto.cs similarity index 62% rename from backend/src/Host/Models/Friends/FriendDto.cs rename to backend/src/Modules/Relations/Application/Friends/DTOs/FriendDto.cs index a7bc80a..6a2a6c6 100644 --- a/backend/src/Host/Models/Friends/FriendDto.cs +++ b/backend/src/Modules/Relations/Application/Friends/DTOs/FriendDto.cs @@ -1,6 +1,7 @@ using System; -namespace Host.Models; +using Knot.Modules.Relations.Application.Abstractions; +namespace Knot.Modules.Relations.Application.Friends.DTOs; public record FriendDto( Guid Id, diff --git a/backend/src/Host/Models/Friends/FriendRequestDto.cs b/backend/src/Modules/Relations/Application/Friends/DTOs/FriendRequestDto.cs similarity index 65% rename from backend/src/Host/Models/Friends/FriendRequestDto.cs rename to backend/src/Modules/Relations/Application/Friends/DTOs/FriendRequestDto.cs index ab502df..423fb00 100644 --- a/backend/src/Host/Models/Friends/FriendRequestDto.cs +++ b/backend/src/Modules/Relations/Application/Friends/DTOs/FriendRequestDto.cs @@ -1,6 +1,7 @@ using System; -namespace Host.Models; +using Knot.Modules.Relations.Application.Abstractions; +namespace Knot.Modules.Relations.Application.Friends.DTOs; public record FriendRequestDto( Guid Id, diff --git a/backend/src/Modules/Relations/Application/Friends/DTOs/SendFriendRequest.cs b/backend/src/Modules/Relations/Application/Friends/DTOs/SendFriendRequest.cs new file mode 100644 index 0000000..b5227c2 --- /dev/null +++ b/backend/src/Modules/Relations/Application/Friends/DTOs/SendFriendRequest.cs @@ -0,0 +1,6 @@ +using System; + +using Knot.Modules.Relations.Application.Abstractions; +namespace Knot.Modules.Relations.Application.Friends.DTOs; + +public record SendFriendRequest(Guid FriendId); diff --git a/backend/src/Modules/Identity/Application/Friends/DeclineFriendRequest.cs b/backend/src/Modules/Relations/Application/Friends/DeclineFriendRequest.cs similarity index 71% rename from backend/src/Modules/Identity/Application/Friends/DeclineFriendRequest.cs rename to backend/src/Modules/Relations/Application/Friends/DeclineFriendRequest.cs index 803e0e7..3dbf73a 100644 --- a/backend/src/Modules/Identity/Application/Friends/DeclineFriendRequest.cs +++ b/backend/src/Modules/Relations/Application/Friends/DeclineFriendRequest.cs @@ -1,20 +1,21 @@ -using Knot.Modules.Identity.Domain; +using Knot.Modules.Relations.Domain; using System; using System.Threading; using System.Threading.Tasks; -using Knot.Modules.Identity.Application.Abstractions; + using Knot.Shared.Kernel; -namespace Knot.Modules.Identity.Application.Friends; +using Knot.Modules.Relations.Application.Abstractions; +namespace Knot.Modules.Relations.Application.Friends; public record DeclineFriendRequestCommand(Guid UserId, Guid FriendshipId) : ICommand; internal sealed class DeclineFriendRequestCommandHandler : ICommandHandler { - private readonly IIdentityDbContext _context; - private readonly IIdentityUnitOfWork _unitOfWork; + private readonly IRelationsDbContext _context; + private readonly IRelationsUnitOfWork _unitOfWork; - public DeclineFriendRequestCommandHandler(IIdentityDbContext context, IIdentityUnitOfWork unitOfWork) + public DeclineFriendRequestCommandHandler(IRelationsDbContext context, IRelationsUnitOfWork unitOfWork) { _context = context; _unitOfWork = unitOfWork; diff --git a/backend/src/Modules/Identity/Application/Friends/FriendDto.cs b/backend/src/Modules/Relations/Application/Friends/FriendDto.cs similarity index 67% rename from backend/src/Modules/Identity/Application/Friends/FriendDto.cs rename to backend/src/Modules/Relations/Application/Friends/FriendDto.cs index 9aceef6..60a60cc 100644 --- a/backend/src/Modules/Identity/Application/Friends/FriendDto.cs +++ b/backend/src/Modules/Relations/Application/Friends/FriendDto.cs @@ -1,7 +1,8 @@ using System; using System.Collections.Generic; -namespace Knot.Modules.Identity.Application.Friends; +using Knot.Modules.Relations.Application.Abstractions; +namespace Knot.Modules.Relations.Application.Friends; public record FriendDto( Guid Id, diff --git a/backend/src/Modules/Identity/Application/Friends/FriendRequestDto.cs b/backend/src/Modules/Relations/Application/Friends/FriendRequestDto.cs similarity index 65% rename from backend/src/Modules/Identity/Application/Friends/FriendRequestDto.cs rename to backend/src/Modules/Relations/Application/Friends/FriendRequestDto.cs index 30c6bf3..f98e15d 100644 --- a/backend/src/Modules/Identity/Application/Friends/FriendRequestDto.cs +++ b/backend/src/Modules/Relations/Application/Friends/FriendRequestDto.cs @@ -1,6 +1,7 @@ using System; -namespace Knot.Modules.Identity.Application.Friends; +using Knot.Modules.Relations.Application.Abstractions; +namespace Knot.Modules.Relations.Application.Friends; public record FriendUserDto(Guid Id, string Username, string DisplayName, string? Avatar); diff --git a/backend/src/Modules/Identity/Application/Friends/FriendshipStatusResponse.cs b/backend/src/Modules/Relations/Application/Friends/FriendshipStatusResponse.cs similarity index 55% rename from backend/src/Modules/Identity/Application/Friends/FriendshipStatusResponse.cs rename to backend/src/Modules/Relations/Application/Friends/FriendshipStatusResponse.cs index 05062d2..6d10596 100644 --- a/backend/src/Modules/Identity/Application/Friends/FriendshipStatusResponse.cs +++ b/backend/src/Modules/Relations/Application/Friends/FriendshipStatusResponse.cs @@ -1,6 +1,7 @@ using System; -namespace Knot.Modules.Identity.Application.Friends; +using Knot.Modules.Relations.Application.Abstractions; +namespace Knot.Modules.Relations.Application.Friends; public record FriendshipStatusResponse( string Status, diff --git a/backend/src/Modules/Identity/Application/Friends/GetFriends.cs b/backend/src/Modules/Relations/Application/Friends/GetFriends.cs similarity index 85% rename from backend/src/Modules/Identity/Application/Friends/GetFriends.cs rename to backend/src/Modules/Relations/Application/Friends/GetFriends.cs index ac99079..917d45f 100644 --- a/backend/src/Modules/Identity/Application/Friends/GetFriends.cs +++ b/backend/src/Modules/Relations/Application/Friends/GetFriends.cs @@ -3,22 +3,23 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Knot.Modules.Identity.Application.Abstractions; -using Knot.Modules.Identity.Domain; + +using Knot.Modules.Relations.Domain; using Knot.Shared.Kernel; using MediatR; using Microsoft.EntityFrameworkCore; -namespace Knot.Modules.Identity.Application.Friends; +using Knot.Modules.Relations.Application.Abstractions; +namespace Knot.Modules.Relations.Application.Friends; public record GetFriendsQuery(Guid UserId) : IQuery>; internal sealed class GetFriendsQueryHandler : IQueryHandler> { - private readonly IIdentityDbContext _context; + private readonly IRelationsDbContext _context; private readonly IUserRepository _userRepository; - public GetFriendsQueryHandler(IIdentityDbContext context, IUserRepository userRepository) + public GetFriendsQueryHandler(IRelationsDbContext context, IUserRepository userRepository) { _context = context; _userRepository = userRepository; diff --git a/backend/src/Modules/Identity/Application/Friends/GetFriendshipStatus.cs b/backend/src/Modules/Relations/Application/Friends/GetFriendshipStatus.cs similarity index 85% rename from backend/src/Modules/Identity/Application/Friends/GetFriendshipStatus.cs rename to backend/src/Modules/Relations/Application/Friends/GetFriendshipStatus.cs index b34dae5..61cd5d1 100644 --- a/backend/src/Modules/Identity/Application/Friends/GetFriendshipStatus.cs +++ b/backend/src/Modules/Relations/Application/Friends/GetFriendshipStatus.cs @@ -1,19 +1,20 @@ using System; using System.Threading; using System.Threading.Tasks; -using Knot.Modules.Identity.Application.Abstractions; + using Knot.Shared.Kernel; using Microsoft.EntityFrameworkCore; -namespace Knot.Modules.Identity.Application.Friends; +using Knot.Modules.Relations.Application.Abstractions; +namespace Knot.Modules.Relations.Application.Friends; public record GetFriendshipStatusQuery(Guid CurrentUserId, Guid TargetUserId) : IQuery; internal sealed class GetFriendshipStatusQueryHandler : IQueryHandler { - private readonly IIdentityDbContext _context; + private readonly IRelationsDbContext _context; - public GetFriendshipStatusQueryHandler(IIdentityDbContext context) + public GetFriendshipStatusQueryHandler(IRelationsDbContext context) { _context = context; } diff --git a/backend/src/Modules/Identity/Application/Friends/GetIncomingRequests.cs b/backend/src/Modules/Relations/Application/Friends/GetIncomingRequests.cs similarity index 82% rename from backend/src/Modules/Identity/Application/Friends/GetIncomingRequests.cs rename to backend/src/Modules/Relations/Application/Friends/GetIncomingRequests.cs index e6b6e1d..a8face8 100644 --- a/backend/src/Modules/Identity/Application/Friends/GetIncomingRequests.cs +++ b/backend/src/Modules/Relations/Application/Friends/GetIncomingRequests.cs @@ -3,21 +3,22 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Knot.Modules.Identity.Application.Abstractions; -using Knot.Modules.Identity.Domain; + +using Knot.Modules.Relations.Domain; using Knot.Shared.Kernel; using Microsoft.EntityFrameworkCore; -namespace Knot.Modules.Identity.Application.Friends; +using Knot.Modules.Relations.Application.Abstractions; +namespace Knot.Modules.Relations.Application.Friends; public record GetIncomingRequestsQuery(Guid UserId) : IQuery>; internal sealed class GetIncomingRequestsQueryHandler : IQueryHandler> { - private readonly IIdentityDbContext _context; + private readonly IRelationsDbContext _context; private readonly IUserRepository _userRepository; - public GetIncomingRequestsQueryHandler(IIdentityDbContext context, IUserRepository userRepository) + public GetIncomingRequestsQueryHandler(IRelationsDbContext context, IUserRepository userRepository) { _context = context; _userRepository = userRepository; diff --git a/backend/src/Modules/Identity/Application/Friends/GetOutgoingRequests.cs b/backend/src/Modules/Relations/Application/Friends/GetOutgoingRequests.cs similarity index 82% rename from backend/src/Modules/Identity/Application/Friends/GetOutgoingRequests.cs rename to backend/src/Modules/Relations/Application/Friends/GetOutgoingRequests.cs index 6986b00..65fd442 100644 --- a/backend/src/Modules/Identity/Application/Friends/GetOutgoingRequests.cs +++ b/backend/src/Modules/Relations/Application/Friends/GetOutgoingRequests.cs @@ -3,21 +3,22 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Knot.Modules.Identity.Application.Abstractions; -using Knot.Modules.Identity.Domain; + +using Knot.Modules.Relations.Domain; using Knot.Shared.Kernel; using Microsoft.EntityFrameworkCore; -namespace Knot.Modules.Identity.Application.Friends; +using Knot.Modules.Relations.Application.Abstractions; +namespace Knot.Modules.Relations.Application.Friends; public record GetOutgoingRequestsQuery(Guid UserId) : IQuery>; internal sealed class GetOutgoingRequestsQueryHandler : IQueryHandler> { - private readonly IIdentityDbContext _context; + private readonly IRelationsDbContext _context; private readonly IUserRepository _userRepository; - public GetOutgoingRequestsQueryHandler(IIdentityDbContext context, IUserRepository userRepository) + public GetOutgoingRequestsQueryHandler(IRelationsDbContext context, IUserRepository userRepository) { _context = context; _userRepository = userRepository; diff --git a/backend/src/Modules/Identity/Application/Friends/RemoveFriend.cs b/backend/src/Modules/Relations/Application/Friends/RemoveFriend.cs similarity index 72% rename from backend/src/Modules/Identity/Application/Friends/RemoveFriend.cs rename to backend/src/Modules/Relations/Application/Friends/RemoveFriend.cs index ce772c0..5987a9c 100644 --- a/backend/src/Modules/Identity/Application/Friends/RemoveFriend.cs +++ b/backend/src/Modules/Relations/Application/Friends/RemoveFriend.cs @@ -1,20 +1,21 @@ -using Knot.Modules.Identity.Domain; +using Knot.Modules.Relations.Domain; using System; using System.Threading; using System.Threading.Tasks; -using Knot.Modules.Identity.Application.Abstractions; + using Knot.Shared.Kernel; -namespace Knot.Modules.Identity.Application.Friends; +using Knot.Modules.Relations.Application.Abstractions; +namespace Knot.Modules.Relations.Application.Friends; public record RemoveFriendCommand(Guid UserId, Guid FriendshipId) : ICommand; internal sealed class RemoveFriendCommandHandler : ICommandHandler { - private readonly IIdentityDbContext _context; - private readonly IIdentityUnitOfWork _unitOfWork; + private readonly IRelationsDbContext _context; + private readonly IRelationsUnitOfWork _unitOfWork; - public RemoveFriendCommandHandler(IIdentityDbContext context, IIdentityUnitOfWork unitOfWork) + public RemoveFriendCommandHandler(IRelationsDbContext context, IRelationsUnitOfWork unitOfWork) { _context = context; _unitOfWork = unitOfWork; diff --git a/backend/src/Modules/Identity/Application/Friends/SendFriendRequest.cs b/backend/src/Modules/Relations/Application/Friends/SendFriendRequest.cs similarity index 77% rename from backend/src/Modules/Identity/Application/Friends/SendFriendRequest.cs rename to backend/src/Modules/Relations/Application/Friends/SendFriendRequest.cs index d552ddc..b62214c 100644 --- a/backend/src/Modules/Identity/Application/Friends/SendFriendRequest.cs +++ b/backend/src/Modules/Relations/Application/Friends/SendFriendRequest.cs @@ -1,21 +1,22 @@ using System; using System.Threading; using System.Threading.Tasks; -using Knot.Modules.Identity.Application.Abstractions; -using Knot.Modules.Identity.Domain; + +using Knot.Modules.Relations.Domain; using Knot.Shared.Kernel; using Microsoft.EntityFrameworkCore; -namespace Knot.Modules.Identity.Application.Friends; +using Knot.Modules.Relations.Application.Abstractions; +namespace Knot.Modules.Relations.Application.Friends; public record SendFriendRequestCommand(Guid UserId, Guid FriendId) : ICommand; internal sealed class SendFriendRequestCommandHandler : ICommandHandler { - private readonly IIdentityDbContext _context; - private readonly IIdentityUnitOfWork _unitOfWork; + private readonly IRelationsDbContext _context; + private readonly IRelationsUnitOfWork _unitOfWork; - public SendFriendRequestCommandHandler(IIdentityDbContext context, IIdentityUnitOfWork unitOfWork) + public SendFriendRequestCommandHandler(IRelationsDbContext context, IRelationsUnitOfWork unitOfWork) { _context = context; _unitOfWork = unitOfWork; diff --git a/backend/src/Modules/Identity/Domain/Friendship.cs b/backend/src/Modules/Relations/Domain/Friendship.cs similarity index 95% rename from backend/src/Modules/Identity/Domain/Friendship.cs rename to backend/src/Modules/Relations/Domain/Friendship.cs index 1552a2f..b053d30 100644 --- a/backend/src/Modules/Identity/Domain/Friendship.cs +++ b/backend/src/Modules/Relations/Domain/Friendship.cs @@ -1,6 +1,6 @@ using Knot.Shared.Kernel; -namespace Knot.Modules.Identity.Domain; +namespace Knot.Modules.Relations.Domain; public enum FriendshipStatus { diff --git a/backend/src/Modules/Relations/Domain/IdentityErrors.cs b/backend/src/Modules/Relations/Domain/IdentityErrors.cs new file mode 100644 index 0000000..2d17e43 --- /dev/null +++ b/backend/src/Modules/Relations/Domain/IdentityErrors.cs @@ -0,0 +1,10 @@ +namespace Knot.Modules.Relations.Domain; +using Knot.Shared.Kernel; + +public static class IdentityErrors +{ + public static readonly Error UserNotFound = new("User.NotFound", "User not found"); + public static readonly Error FriendsNotFound = new("Friends.NotFound", "Friendship not found"); + public static readonly Error FriendsSelf = new("Friends.Self", "Cannot add self"); + public static readonly Error FriendsExists = new("Friends.Exists", "Friendship already exists"); +} diff --git a/backend/src/Modules/Relations/Domain/UserReplica.cs b/backend/src/Modules/Relations/Domain/UserReplica.cs new file mode 100644 index 0000000..452886a --- /dev/null +++ b/backend/src/Modules/Relations/Domain/UserReplica.cs @@ -0,0 +1,9 @@ +namespace Knot.Modules.Relations.Domain; +using System; +public class UserReplica +{ + public Guid Id { get; set; } + public string Username { get; set; } + public string DisplayName { get; set; } + public string Avatar { get; set; } +} diff --git a/backend/src/Modules/Relations/GlobalUsings.cs b/backend/src/Modules/Relations/GlobalUsings.cs new file mode 100644 index 0000000..d29b208 --- /dev/null +++ b/backend/src/Modules/Relations/GlobalUsings.cs @@ -0,0 +1,2 @@ +global using Knot.Modules.Relations.Application.Abstractions; +global using Knot.Modules.Relations.Domain; diff --git a/backend/src/Modules/Relations/Knot.Modules.Relations.csproj b/backend/src/Modules/Relations/Knot.Modules.Relations.csproj new file mode 100644 index 0000000..c293968 --- /dev/null +++ b/backend/src/Modules/Relations/Knot.Modules.Relations.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/Modules/Relations/Presentation/Endpoints/FriendsEndpoints.cs b/backend/src/Modules/Relations/Presentation/Endpoints/FriendsEndpoints.cs new file mode 100644 index 0000000..ab4b56d --- /dev/null +++ b/backend/src/Modules/Relations/Presentation/Endpoints/FriendsEndpoints.cs @@ -0,0 +1,67 @@ +using Carter; +using Knot.Shared.Kernel; +using Knot.Modules.Relations.Application.Friends; +using Knot.Modules.Relations.Application.Friends.DTOs; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Mvc; + +namespace Knot.Modules.Relations.Presentation.Endpoints; + +public sealed class FriendsEndpoints : ICarterModule +{ + public void AddRoutes(IEndpointRouteBuilder app) + { + var group = app.MapGroup("api/friends").RequireAuthorization(); + + group.MapGet("", async (ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new GetFriendsQuery(userContext.UserId), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error); + }); + + group.MapGet("requests", async (ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new GetIncomingRequestsQuery(userContext.UserId), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error); + }); + + group.MapGet("outgoing", async (ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new GetOutgoingRequestsQuery(userContext.UserId), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error); + }); + + group.MapPost("request", async ([FromBody] SendFriendRequest request, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new SendFriendRequestCommand(userContext.UserId, request.FriendId), ct); + return result.IsSuccess ? Results.Ok(new { status = "pending" }) : Results.BadRequest(result.Error.Description); + }); + + group.MapPost("{id:guid}/accept", async (Guid id, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new AcceptFriendRequestCommand(userContext.UserId, id), ct); + return result.IsSuccess ? Results.Ok(new { id = result.Value }) : Results.NotFound(result.Error.Description); + }); + + group.MapPost("{id:guid}/decline", async (Guid id, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new DeclineFriendRequestCommand(userContext.UserId, id), ct); + return result.IsSuccess ? Results.Ok(new Knot.Shared.Kernel.SuccessResponse(true)) : Results.NotFound(result.Error.Description); + }); + + group.MapGet("status/{userId:guid}", async (Guid userId, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new GetFriendshipStatusQuery(userContext.UserId, userId), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error); + }); + + group.MapDelete("{id:guid}", async (Guid id, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new RemoveFriendCommand(userContext.UserId, id), ct); + return result.IsSuccess ? Results.Ok(new Knot.Shared.Kernel.SuccessResponse(true)) : Results.NotFound(result.Error.Description); + }); + } +} diff --git a/backend/src/Host/Models/Config/PublicConfigDto.cs b/backend/src/Modules/Settings/Application/Config/DTOs/PublicConfigDto.cs similarity index 93% rename from backend/src/Host/Models/Config/PublicConfigDto.cs rename to backend/src/Modules/Settings/Application/Config/DTOs/PublicConfigDto.cs index c40fc96..e13d239 100644 --- a/backend/src/Host/Models/Config/PublicConfigDto.cs +++ b/backend/src/Modules/Settings/Application/Config/DTOs/PublicConfigDto.cs @@ -1,6 +1,6 @@ using Knot.Shared.Kernel.Configuration; -namespace Host.Models; +namespace Knot.Modules.Settings.Application.Config.DTOs; public record PublicConfigDto { @@ -23,4 +23,4 @@ public record PublicConfigDto EnableRegistration = settings.EnableRegistration }; } -} +} \ No newline at end of file diff --git a/backend/src/Host/Application/Config/Queries/GetPublicConfigQuery.cs b/backend/src/Modules/Settings/Application/Config/Queries/GetPublicConfigQuery.cs similarity index 83% rename from backend/src/Host/Application/Config/Queries/GetPublicConfigQuery.cs rename to backend/src/Modules/Settings/Application/Config/Queries/GetPublicConfigQuery.cs index 2f405ea..214811d 100644 --- a/backend/src/Host/Application/Config/Queries/GetPublicConfigQuery.cs +++ b/backend/src/Modules/Settings/Application/Config/Queries/GetPublicConfigQuery.cs @@ -1,7 +1,10 @@ +using Knot.Modules.Settings.Application.Config.DTOs; using Knot.Shared.Kernel; using Knot.Shared.Kernel.Configuration; using MediatR; -using Host.Models; +using Knot.Modules.Auth.Application.Auth.DTOs; + +using Knot.Modules.Auth.Application.Users; using System.Threading; using System.Threading.Tasks; @@ -22,4 +25,4 @@ internal sealed class GetPublicConfigQueryHandler : IQueryHandler + + net10.0 + enable + enable + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/Modules/Settings/Presentation/Endpoints/SettingsEndpoints.cs b/backend/src/Modules/Settings/Presentation/Endpoints/SettingsEndpoints.cs new file mode 100644 index 0000000..ed43cef --- /dev/null +++ b/backend/src/Modules/Settings/Presentation/Endpoints/SettingsEndpoints.cs @@ -0,0 +1,25 @@ +using Carter; +using MediatR; +using Knot.Shared.Kernel.Constants; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace Host.Endpoints; + +/// +/// Регистрация эндпоинтов для конфигурации приложения. +/// +public sealed class SettingsEndpoints : ICarterModule +{ + public void AddRoutes(IEndpointRouteBuilder app) + { + var group = app.MapGroup(Routes.ApiConfig); + + group.MapGet("/", async (ISender sender, CancellationToken ct) => + { + var result = await sender.Send(new Host.Application.Config.Queries.GetPublicConfigQuery(), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error); + }); + } +} \ No newline at end of file diff --git a/backend/src/Modules/Storage/DependencyInjection.cs b/backend/src/Modules/Storage/DependencyInjection.cs new file mode 100644 index 0000000..05bce71 --- /dev/null +++ b/backend/src/Modules/Storage/DependencyInjection.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Minio; +using Knot.Shared.Kernel.Storage; +using Knot.Shared.Kernel.Security; +using Knot.Modules.Storage.Infrastructure.Storage; +using System; + +namespace Knot.Modules.Storage; + +public static class DependencyInjection +{ + public static IServiceCollection AddStorageModule(this IServiceCollection services, IConfiguration configuration) + { + var s3Endpoint = configuration["S3_ENDPOINT"] ?? "minio:9000"; + var s3AccessKey = configuration["S3_ACCESS_KEY"] ?? "admin"; + var s3SecretKey = configuration["S3_SECRET_KEY"] ?? "KnotSuperSecretMinioPassword"; + var s3Bucket = configuration["S3_BUCKET"] ?? "knot-uploads"; + + services.AddMinio(configureClient => configureClient + .WithEndpoint(s3Endpoint) + .WithCredentials(s3AccessKey, s3SecretKey) + .WithSSL(false) + .Build()); + + services.AddScoped(provider => + { + var minioClient = provider.GetRequiredService(); + var encService = provider.GetRequiredService(); + return new S3FileStorageService(minioClient, encService, s3Bucket); + }); + + return services; + } +} diff --git a/backend/src/Shared/Knot.Shared.Infrastructure/Storage/S3FileStorageService.cs b/backend/src/Modules/Storage/Infrastructure/Storage/S3FileStorageService.cs similarity index 88% rename from backend/src/Shared/Knot.Shared.Infrastructure/Storage/S3FileStorageService.cs rename to backend/src/Modules/Storage/Infrastructure/Storage/S3FileStorageService.cs index 4243191..0942a24 100644 --- a/backend/src/Shared/Knot.Shared.Infrastructure/Storage/S3FileStorageService.cs +++ b/backend/src/Modules/Storage/Infrastructure/Storage/S3FileStorageService.cs @@ -8,7 +8,9 @@ using System.Text.Json; using System.Threading.Tasks; using Knot.Shared.Kernel.Security; -namespace Knot.Shared.Kernel.Storage; +using Knot.Shared.Kernel.Storage; + +namespace Knot.Modules.Storage.Infrastructure.Storage; public class S3FileStorageService : IFileStorageService { @@ -82,9 +84,10 @@ public class S3FileStorageService : IFileStorageService // 4. Загружаем зашифрованный файл в MinIO using (var fileToUpload = new FileStream(tempEncryptedPath, FileMode.Open, FileAccess.Read)) { + var actualContentType = contentType ?? "application/octet-stream"; var metadata = new System.Collections.Generic.Dictionary { - { "ContentType", contentType ?? "application/octet-stream" }, + { "ContentType", actualContentType }, { "OriginalFileName", Uri.EscapeDataString(fileName ?? "unknown") }, { "IV", Convert.ToBase64String(ivParams) }, { "KeyVersion", "1" }, @@ -96,7 +99,7 @@ public class S3FileStorageService : IFileStorageService .WithObject(fileId) .WithStreamData(fileToUpload) .WithObjectSize(fileToUpload.Length) - .WithContentType("application/octet-stream") + .WithContentType(actualContentType) .WithHeaders(metadata); await _minioClient.PutObjectAsync(putObjectArgs).ConfigureAwait(false); @@ -215,4 +218,28 @@ public class S3FileStorageService : IFileStorageService } return result; } + + public async Task GetFileSizeAsync(string fileId, System.Threading.CancellationToken ct = default) + { + try + { + var statArgs = new StatObjectArgs().WithBucket(_bucketName).WithObject(fileId); + var stat = await _minioClient.StatObjectAsync(statArgs).ConfigureAwait(false); + return stat.Size; + } + catch + { + return 0; + } + } + + public async Task> GetFileSizesAsync(System.Collections.Generic.IEnumerable fileIds, System.Threading.CancellationToken ct = default) + { + var result = new System.Collections.Generic.Dictionary(); + foreach (var fileId in fileIds) + { + result[fileId] = await GetFileSizeAsync(fileId, ct); + } + return result; + } } diff --git a/backend/src/Modules/Storage/Knot.Modules.Storage.csproj b/backend/src/Modules/Storage/Knot.Modules.Storage.csproj new file mode 100644 index 0000000..7afe63f --- /dev/null +++ b/backend/src/Modules/Storage/Knot.Modules.Storage.csproj @@ -0,0 +1,22 @@ + + + + + + + + net10.0 + enable + enable + + + + + + + + + + + + diff --git a/backend/src/Modules/Storage/Presentation/Endpoints/FilesEndpoints.cs b/backend/src/Modules/Storage/Presentation/Endpoints/FilesEndpoints.cs new file mode 100644 index 0000000..fe174fb --- /dev/null +++ b/backend/src/Modules/Storage/Presentation/Endpoints/FilesEndpoints.cs @@ -0,0 +1,53 @@ +using Carter; +using Microsoft.Extensions.Caching.Memory; +using Knot.Shared.Kernel.Storage; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Mvc; + +namespace Knot.Host.Presentation.Endpoints; + +public sealed class FilesEndpoints : ICarterModule +{ + public void AddRoutes(IEndpointRouteBuilder app) + { + var group = app.MapGroup("api/files"); + + group.MapGet("{id}", async (string id, [FromQuery] bool download, IFileStorageService fileStorage, IMemoryCache cache, CancellationToken ct) => + { + try + { + var cacheKey = $"file_{id}"; + if (!cache.TryGetValue(cacheKey, out (byte[] Bytes, string ContentType, string FileName) cachedData)) + { + var result = await fileStorage.DownloadFileAsync(id); + using var ms = new MemoryStream(); + if (result.Stream.CanSeek) result.Stream.Position = 0; + await result.Stream.CopyToAsync(ms, ct); + + cachedData = (ms.ToArray(), result.ContentType, result.FileName); + + var cacheEntryOptions = new MemoryCacheEntryOptions() + .SetSlidingExpiration(TimeSpan.FromMinutes(30)); + + cache.Set(cacheKey, cachedData, cacheEntryOptions); + result.Stream.Dispose(); + } + + var outStream = new MemoryStream(cachedData.Bytes); + + if (download && !string.IsNullOrEmpty(cachedData.FileName)) + { + return Results.File(outStream, cachedData.ContentType, fileDownloadName: cachedData.FileName, enableRangeProcessing: true); + } + + return Results.File(outStream, cachedData.ContentType, enableRangeProcessing: true); + } + catch (Exception ex) + { + return Results.NotFound(new { error = "Файл не найден или ошибка доступа.", message = ex.Message }); + } + }); + } +} diff --git a/backend/src/Modules/Stories/Application/Abstractions/IStoriesDbContext.cs b/backend/src/Modules/Stories/Application/Abstractions/IStoriesDbContext.cs new file mode 100644 index 0000000..fdff347 --- /dev/null +++ b/backend/src/Modules/Stories/Application/Abstractions/IStoriesDbContext.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Knot.Modules.Stories.Domain; +using Knot.Modules.Relations.Domain; + +namespace Knot.Modules.Stories.Application.Abstractions; + +public interface IStoriesDbContext +{ + DbSet Stories { get; } + DbSet Friendships { get; } + DbSet StoryViewers { get; } + DbSet StoryReactions { get; } + DbSet StoryReplies { get; } + DbSet Set() where TEntity : class; + Task SaveChangesAsync(CancellationToken cancellationToken); +} + +public interface IStoriesUnitOfWork +{ + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/backend/src/Modules/Stories/Application/Abstractions/IUserRepository.cs b/backend/src/Modules/Stories/Application/Abstractions/IUserRepository.cs new file mode 100644 index 0000000..382f251 --- /dev/null +++ b/backend/src/Modules/Stories/Application/Abstractions/IUserRepository.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Knot.Modules.Stories.Domain; + +namespace Knot.Modules.Stories.Application.Abstractions; + +public interface IUserRepository +{ + Task ExistsAsync(Guid id, CancellationToken cancellationToken = default); + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); +} diff --git a/backend/src/Host/Application/Stories/Commands/AddStoryReaction/AddStoryReactionCommand.cs b/backend/src/Modules/Stories/Application/Stories/Commands/AddStoryReaction/AddStoryReactionCommand.cs similarity index 87% rename from backend/src/Host/Application/Stories/Commands/AddStoryReaction/AddStoryReactionCommand.cs rename to backend/src/Modules/Stories/Application/Stories/Commands/AddStoryReaction/AddStoryReactionCommand.cs index 5ededd7..50519e9 100644 --- a/backend/src/Host/Application/Stories/Commands/AddStoryReaction/AddStoryReactionCommand.cs +++ b/backend/src/Modules/Stories/Application/Stories/Commands/AddStoryReaction/AddStoryReactionCommand.cs @@ -1,3 +1,4 @@ +using Knot.Modules.Messaging.Domain; using Host.Application.Stories; using System; using System.Collections.Generic; @@ -6,23 +7,31 @@ using System.Threading; using System.Threading.Tasks; using MediatR; using Knot.Shared.Kernel; -using Knot.Modules.Identity.Domain; -using Knot.Modules.Identity.Infrastructure.Persistence; -using Knot.Modules.Identity.Application.Abstractions; -using Knot.Modules.Chats.Domain; -using Knot.Modules.Chats.Application.Messages.Send; +using Knot.Modules.Stories.Domain; + + +using Knot.Modules.Conversations.Domain; +using Knot.Modules.Conversations.Application.Messages.Send; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.SignalR; -using Knot.Modules.Chats.Infrastructure.SignalR; -using Host.Models; +using Knot.Modules.Conversations.Infrastructure.SignalR; + + + + +using Knot.Modules.Stories.Application.DTOs; + + + +using Knot.Modules.Messaging.Application.Abstractions; namespace Host.Application.Stories.Commands.AddStoryReaction; public record AddStoryReactionCommand(Guid UserId, Guid StoryId, string Emoji) : ICommand; internal sealed class AddStoryReactionCommandHandler : ICommandHandler { - private readonly IdentityDbContext _context; + private readonly IStoriesDbContext _context; private readonly IUserRepository _userRepository; private readonly IChatRepository _chatRepository; private readonly IMessageRepository _messageRepository; @@ -30,7 +39,7 @@ internal sealed class AddStoryReactionCommandHandler : ICommandHandler _hubContext; public AddStoryReactionCommandHandler( - IdentityDbContext context, + IStoriesDbContext context, IUserRepository userRepository, IChatRepository chatRepository, IMessageRepository messageRepository, @@ -70,7 +79,7 @@ internal sealed class AddStoryReactionCommandHandler : ICommandHandler { userId1, userId2 }); + var command = new Knot.Modules.Conversations.Application.Chats.Create.CreateChatCommand(string.Empty, ChatType.Personal, new List { userId1, userId2 }); var result = await _sender.Send(command, cancellationToken); return result.Value; } -} \ No newline at end of file +} + + diff --git a/backend/src/Host/Application/Stories/Commands/AddStoryReply/AddStoryReplyCommand.cs b/backend/src/Modules/Stories/Application/Stories/Commands/AddStoryReply/AddStoryReplyCommand.cs similarity index 87% rename from backend/src/Host/Application/Stories/Commands/AddStoryReply/AddStoryReplyCommand.cs rename to backend/src/Modules/Stories/Application/Stories/Commands/AddStoryReply/AddStoryReplyCommand.cs index d57222b..08a5bfe 100644 --- a/backend/src/Host/Application/Stories/Commands/AddStoryReply/AddStoryReplyCommand.cs +++ b/backend/src/Modules/Stories/Application/Stories/Commands/AddStoryReply/AddStoryReplyCommand.cs @@ -1,3 +1,4 @@ +using Knot.Modules.Messaging.Domain; using Host.Application.Stories; using System; using System.Collections.Generic; @@ -6,23 +7,31 @@ using System.Threading; using System.Threading.Tasks; using MediatR; using Knot.Shared.Kernel; -using Knot.Modules.Identity.Domain; -using Knot.Modules.Identity.Infrastructure.Persistence; -using Knot.Modules.Identity.Application.Abstractions; -using Knot.Modules.Chats.Domain; -using Knot.Modules.Chats.Application.Messages.Send; +using Knot.Modules.Stories.Domain; + + +using Knot.Modules.Conversations.Domain; +using Knot.Modules.Conversations.Application.Messages.Send; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.SignalR; -using Knot.Modules.Chats.Infrastructure.SignalR; -using Host.Models; +using Knot.Modules.Conversations.Infrastructure.SignalR; + + + + +using Knot.Modules.Stories.Application.DTOs; + + + +using Knot.Modules.Messaging.Application.Abstractions; namespace Host.Application.Stories.Commands.AddStoryReply; public record AddStoryReplyCommand(Guid UserId, Guid StoryId, string Content) : ICommand; internal sealed class AddStoryReplyCommandHandler : ICommandHandler { - private readonly IdentityDbContext _context; + private readonly IStoriesDbContext _context; private readonly IUserRepository _userRepository; private readonly IChatRepository _chatRepository; private readonly IMessageRepository _messageRepository; @@ -30,7 +39,7 @@ internal sealed class AddStoryReplyCommandHandler : ICommandHandler _hubContext; public AddStoryReplyCommandHandler( - IdentityDbContext context, + IStoriesDbContext context, IUserRepository userRepository, IChatRepository chatRepository, IMessageRepository messageRepository, @@ -117,8 +126,10 @@ internal sealed class AddStoryReplyCommandHandler : ICommandHandler { userId1, userId2 }); + var command = new Knot.Modules.Conversations.Application.Chats.Create.CreateChatCommand(string.Empty, ChatType.Personal, new List { userId1, userId2 }); var result = await _sender.Send(command, cancellationToken); return result.Value; } -} \ No newline at end of file +} + + diff --git a/backend/src/Host/Application/Stories/Commands/CreateStory/CreateStoryCommand.cs b/backend/src/Modules/Stories/Application/Stories/Commands/CreateStory/CreateStoryCommand.cs similarity index 79% rename from backend/src/Host/Application/Stories/Commands/CreateStory/CreateStoryCommand.cs rename to backend/src/Modules/Stories/Application/Stories/Commands/CreateStory/CreateStoryCommand.cs index 99028fa..6bb721f 100644 --- a/backend/src/Host/Application/Stories/Commands/CreateStory/CreateStoryCommand.cs +++ b/backend/src/Modules/Stories/Application/Stories/Commands/CreateStory/CreateStoryCommand.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; using MediatR; using Knot.Shared.Kernel; -using Knot.Modules.Identity.Infrastructure.Persistence; -using Knot.Modules.Identity.Domain; + +using Knot.Modules.Stories.Domain; namespace Host.Application.Stories.Commands.CreateStory; @@ -12,9 +12,9 @@ public record CreateStoryCommand(Guid UserId, string Type, string? MediaUrl, str internal sealed class CreateStoryCommandHandler : ICommandHandler { - private readonly IdentityDbContext _context; + private readonly IStoriesDbContext _context; - public CreateStoryCommandHandler(IdentityDbContext context) + public CreateStoryCommandHandler(IStoriesDbContext context) { _context = context; } @@ -34,3 +34,4 @@ internal sealed class CreateStoryCommandHandler : ICommandHandler { - private readonly IdentityDbContext _context; + private readonly IStoriesDbContext _context; - public DeleteStoryCommandHandler(IdentityDbContext context) + public DeleteStoryCommandHandler(IStoriesDbContext context) { _context = context; } @@ -37,4 +44,6 @@ internal sealed class DeleteStoryCommandHandler : ICommandHandler { - private readonly IdentityDbContext _context; + private readonly IStoriesDbContext _context; - public RemoveStoryReactionCommandHandler(IdentityDbContext context) + public RemoveStoryReactionCommandHandler(IStoriesDbContext context) { _context = context; } @@ -36,4 +43,6 @@ internal sealed class RemoveStoryReactionCommandHandler : ICommandHandler { - private readonly IdentityDbContext _context; + private readonly IStoriesDbContext _context; private readonly IUserRepository _userRepository; private readonly IHubContext _hubContext; - public ViewStoryCommandHandler(IdentityDbContext context, IUserRepository userRepository, IHubContext hubContext) + public ViewStoryCommandHandler(IStoriesDbContext context, IUserRepository userRepository, IHubContext hubContext) { _context = context; _userRepository = userRepository; @@ -74,4 +81,6 @@ internal sealed class ViewStoryCommandHandler : ICommandHandler>; internal sealed class GetStoriesQueryHandler : IQueryHandler> { - private readonly IdentityDbContext _context; + private readonly IStoriesDbContext _context; private readonly IUserRepository _userRepository; - public GetStoriesQueryHandler(IdentityDbContext context, IUserRepository userRepository) + public GetStoriesQueryHandler(IStoriesDbContext context, IUserRepository userRepository) { _context = context; _userRepository = userRepository; @@ -96,4 +103,5 @@ internal sealed class GetStoriesQueryHandler : IQueryHandler> { - private readonly IdentityDbContext _context; + private readonly IStoriesDbContext _context; private readonly IUserRepository _userRepository; - public GetStoryRepliesQueryHandler(IdentityDbContext context, IUserRepository userRepository) + public GetStoryRepliesQueryHandler(IStoriesDbContext context, IUserRepository userRepository) { _context = context; _userRepository = userRepository; @@ -63,4 +70,5 @@ internal sealed class GetStoryRepliesQueryHandler : IQueryHandler> { - private readonly IdentityDbContext _context; + private readonly IStoriesDbContext _context; private readonly IUserRepository _userRepository; - public GetStoryViewersQueryHandler(IdentityDbContext context, IUserRepository userRepository) + public GetStoryViewersQueryHandler(IStoriesDbContext context, IUserRepository userRepository) { _context = context; _userRepository = userRepository; @@ -65,4 +72,5 @@ internal sealed class GetStoryViewersQueryHandler : IQueryHandler { - private readonly IdentityDbContext _context; + private readonly IStoriesDbContext _context; private readonly IUserRepository _userRepository; - public GetUserStoriesQueryHandler(IdentityDbContext context, IUserRepository userRepository) + public GetUserStoriesQueryHandler(IStoriesDbContext context, IUserRepository userRepository) { _context = context; _userRepository = userRepository; @@ -38,9 +45,7 @@ internal sealed class GetUserStoriesQueryHandler : IQueryHandler(IdentityErrors.UserNotFound); - } + return Result.Failure(Knot.Modules.Stories.Domain.IdentityErrors.UserNotFound); var result = new StoryGroupDto( new StoryUserDto(user.Id, user.Username, user.DisplayName, user.Avatar), @@ -62,4 +67,5 @@ internal sealed class GetUserStoriesQueryHandler : IQueryHandler { diff --git a/backend/src/Host/Application/Stories/StoryErrors.cs b/backend/src/Modules/Stories/Domain/StoryErrors.cs similarity index 86% rename from backend/src/Host/Application/Stories/StoryErrors.cs rename to backend/src/Modules/Stories/Domain/StoryErrors.cs index 0d05d38..31c2cb8 100644 --- a/backend/src/Host/Application/Stories/StoryErrors.cs +++ b/backend/src/Modules/Stories/Domain/StoryErrors.cs @@ -1,6 +1,6 @@ using Knot.Shared.Kernel; -namespace Host.Application.Stories; +namespace Knot.Modules.Stories.Domain; public static class StoryErrors { diff --git a/backend/src/Modules/Identity/Domain/StoryReaction.cs b/backend/src/Modules/Stories/Domain/StoryReaction.cs similarity index 92% rename from backend/src/Modules/Identity/Domain/StoryReaction.cs rename to backend/src/Modules/Stories/Domain/StoryReaction.cs index 47a48dc..bfbe905 100644 --- a/backend/src/Modules/Identity/Domain/StoryReaction.cs +++ b/backend/src/Modules/Stories/Domain/StoryReaction.cs @@ -1,6 +1,6 @@ using Knot.Shared.Kernel; -namespace Knot.Modules.Identity.Domain; +namespace Knot.Modules.Stories.Domain; public sealed class StoryReaction : Entity { diff --git a/backend/src/Modules/Identity/Domain/StoryReply.cs b/backend/src/Modules/Stories/Domain/StoryReply.cs similarity index 92% rename from backend/src/Modules/Identity/Domain/StoryReply.cs rename to backend/src/Modules/Stories/Domain/StoryReply.cs index 9f1cffc..0c5f393 100644 --- a/backend/src/Modules/Identity/Domain/StoryReply.cs +++ b/backend/src/Modules/Stories/Domain/StoryReply.cs @@ -1,6 +1,6 @@ using Knot.Shared.Kernel; -namespace Knot.Modules.Identity.Domain; +namespace Knot.Modules.Stories.Domain; public sealed class StoryReply : Entity { diff --git a/backend/src/Modules/Identity/Domain/StoryViewer.cs b/backend/src/Modules/Stories/Domain/StoryViewer.cs similarity index 90% rename from backend/src/Modules/Identity/Domain/StoryViewer.cs rename to backend/src/Modules/Stories/Domain/StoryViewer.cs index 8b42f83..5728cbe 100644 --- a/backend/src/Modules/Identity/Domain/StoryViewer.cs +++ b/backend/src/Modules/Stories/Domain/StoryViewer.cs @@ -1,6 +1,6 @@ using Knot.Shared.Kernel; -namespace Knot.Modules.Identity.Domain; +namespace Knot.Modules.Stories.Domain; public sealed class StoryViewer : Entity { diff --git a/backend/src/Modules/Stories/GlobalUsings.cs b/backend/src/Modules/Stories/GlobalUsings.cs new file mode 100644 index 0000000..0d33ed6 --- /dev/null +++ b/backend/src/Modules/Stories/GlobalUsings.cs @@ -0,0 +1,3 @@ +global using Knot.Modules.Stories.Application.Abstractions; +global using Knot.Modules.Stories.Domain; +global using Knot.Modules.Relations.Domain; diff --git a/backend/src/Modules/Stories/Knot.Modules.Stories.csproj b/backend/src/Modules/Stories/Knot.Modules.Stories.csproj new file mode 100644 index 0000000..af5fb80 --- /dev/null +++ b/backend/src/Modules/Stories/Knot.Modules.Stories.csproj @@ -0,0 +1,29 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/Modules/Stories/Presentation/Endpoints/StoriesEndpoints.cs b/backend/src/Modules/Stories/Presentation/Endpoints/StoriesEndpoints.cs new file mode 100644 index 0000000..b50f663 --- /dev/null +++ b/backend/src/Modules/Stories/Presentation/Endpoints/StoriesEndpoints.cs @@ -0,0 +1,91 @@ +using Carter; +using Knot.Modules.Stories.Application.DTOs; +using Knot.Shared.Kernel; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Mvc; +using Host.Application.Stories.Queries.GetStories; +using Host.Application.Stories.Commands.CreateStory; +using Host.Application.Stories.Queries.GetUserStories; +using Host.Application.Stories.Commands.ViewStory; +using Host.Application.Stories.Queries.GetStoryViewers; +using Host.Application.Stories.Commands.AddStoryReaction; +using Host.Application.Stories.Commands.RemoveStoryReaction; +using Host.Application.Stories.Commands.AddStoryReply; +using Host.Application.Stories.Queries.GetStoryReplies; +using Host.Application.Stories.Commands.DeleteStory; + +namespace Knot.Host.Presentation.Endpoints; + +public sealed class StoriesEndpoints : ICarterModule +{ + public void AddRoutes(IEndpointRouteBuilder app) + { + var group = app.MapGroup("api/stories").RequireAuthorization(); + + group.MapGet("", async (ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new GetStoriesQuery(userContext.UserId), ct); + return Results.Ok(result.Value); + }); + + group.MapPost("", async ([FromBody] CreateStoryRequest request, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new CreateStoryCommand(userContext.UserId, request.Type, request.MediaUrl, request.Content, request.BgColor), ct); + return Results.Ok(new { id = result.Value }); + }); + + group.MapGet("user/{userId:guid}", async (Guid userId, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new GetUserStoriesQuery(userContext.UserId, userId), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(); + }); + + group.MapPost("{id:guid}/view", async (Guid id, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new ViewStoryCommand(userContext.UserId, id), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(); + }); + + group.MapGet("{id:guid}/viewers", async (Guid id, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new GetStoryViewersQuery(userContext.UserId, id), ct); + if (result.IsFailure) return result.Error.Code == "Unauthorized" ? Results.Forbid() : Results.NotFound(); + return Results.Ok(result.Value); + }); + + group.MapPost("{id:guid}/reaction", async (Guid id, [FromBody] AddStoryReactionRequest request, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new AddStoryReactionCommand(userContext.UserId, id, request.Emoji), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(); + }); + + group.MapDelete("{id:guid}/reaction", async (Guid id, [FromBody] RemoveStoryReactionRequest request, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new RemoveStoryReactionCommand(userContext.UserId, id, request.Emoji), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(); + }); + + group.MapPost("{id:guid}/reply", async (Guid id, [FromBody] AddStoryReplyRequest request, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new AddStoryReplyCommand(userContext.UserId, id, request.Content), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(); + }); + + group.MapGet("{id:guid}/replies", async (Guid id, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new GetStoryRepliesQuery(userContext.UserId, id), ct); + if (result.IsFailure) return result.Error.Code == "Unauthorized" ? Results.Forbid() : Results.NotFound(); + return Results.Ok(result.Value); + }); + + group.MapDelete("{id:guid}", async (Guid id, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var result = await sender.Send(new DeleteStoryCommand(userContext.UserId, id), ct); + if (result.IsFailure) return result.Error.Code == "Unauthorized" ? Results.Forbid() : Results.NotFound(); + return Results.Ok(result.Value); + }); + } +} diff --git a/backend/src/Modules/Chats/Application/TelegramImport/AnalyzeImportCommand.cs b/backend/src/Modules/TelegramImport/Application/TelegramImport/AnalyzeImportCommand.cs similarity index 95% rename from backend/src/Modules/Chats/Application/TelegramImport/AnalyzeImportCommand.cs rename to backend/src/Modules/TelegramImport/Application/TelegramImport/AnalyzeImportCommand.cs index d507d2a..f96de6d 100644 --- a/backend/src/Modules/Chats/Application/TelegramImport/AnalyzeImportCommand.cs +++ b/backend/src/Modules/TelegramImport/Application/TelegramImport/AnalyzeImportCommand.cs @@ -1,4 +1,5 @@ -using Knot.Modules.Chats.Domain; +using Knot.Modules.Conversations.Domain; +using Knot.Modules.Messaging.Domain; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -12,7 +13,7 @@ using AngleSharp.Dom; using Knot.Shared.Kernel; using MediatR; -namespace Knot.Modules.Chats.Application.TelegramImport; +namespace Knot.Modules.TelegramImport.Application.TelegramImport; public static class TelegramImportState { @@ -91,3 +92,6 @@ internal sealed class AnalyzeImportCommandHandler : ICommandHandler Names ); + + + diff --git a/backend/src/Host/Models/Import/ExecuteImportRequest.cs b/backend/src/Modules/TelegramImport/Application/TelegramImport/DTOs/ExecuteImportRequest.cs similarity index 69% rename from backend/src/Host/Models/Import/ExecuteImportRequest.cs rename to backend/src/Modules/TelegramImport/Application/TelegramImport/DTOs/ExecuteImportRequest.cs index 3ceb4bf..09dd5a1 100644 --- a/backend/src/Host/Models/Import/ExecuteImportRequest.cs +++ b/backend/src/Modules/TelegramImport/Application/TelegramImport/DTOs/ExecuteImportRequest.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; -namespace Host.Models; +namespace Knot.Modules.TelegramImport.Application.TelegramImport.DTOs; public record ExecuteImportRequest( Guid Token, Dictionary Mapping, string? GroupName ); + + + diff --git a/backend/src/Host/Models/Import/ExecuteImportResponseDto.cs b/backend/src/Modules/TelegramImport/Application/TelegramImport/DTOs/ExecuteImportResponseDto.cs similarity index 61% rename from backend/src/Host/Models/Import/ExecuteImportResponseDto.cs rename to backend/src/Modules/TelegramImport/Application/TelegramImport/DTOs/ExecuteImportResponseDto.cs index fa08484..6f85240 100644 --- a/backend/src/Host/Models/Import/ExecuteImportResponseDto.cs +++ b/backend/src/Modules/TelegramImport/Application/TelegramImport/DTOs/ExecuteImportResponseDto.cs @@ -1,9 +1,12 @@ using System; -namespace Host.Models; +namespace Knot.Modules.TelegramImport.Application.TelegramImport.DTOs; public record ExecuteImportResponseDto( bool Success, int MessagesImported, Guid ChatId ); + + + diff --git a/backend/src/Modules/Chats/Application/TelegramImport/ExecuteImportCommand.cs b/backend/src/Modules/TelegramImport/Application/TelegramImport/ExecuteImportCommand.cs similarity index 98% rename from backend/src/Modules/Chats/Application/TelegramImport/ExecuteImportCommand.cs rename to backend/src/Modules/TelegramImport/Application/TelegramImport/ExecuteImportCommand.cs index 4624d39..6691aec 100644 --- a/backend/src/Modules/Chats/Application/TelegramImport/ExecuteImportCommand.cs +++ b/backend/src/Modules/TelegramImport/Application/TelegramImport/ExecuteImportCommand.cs @@ -10,13 +10,16 @@ using AngleSharp.Html.Parser; using MediatR; using Knot.Shared.Kernel; using Knot.Shared.Kernel.Storage; -using Knot.Modules.Chats.Domain; -using Knot.Modules.Chats.Application.Abstractions; -using Knot.Modules.Chats.Application.Chats.Create; -using Knot.Modules.Chats.Infrastructure.SignalR; +using Knot.Modules.Messaging.Domain; +using Knot.Modules.Messaging.Application.Abstractions; +using Knot.Modules.Conversations.Application.Chats.Create; +using Knot.Modules.Conversations.Infrastructure.SignalR; using Microsoft.AspNetCore.SignalR; -namespace Knot.Modules.Chats.Application.TelegramImport; +using Knot.Modules.Conversations.Domain; +using Knot.Modules.Conversations.Application.Abstractions; +using Knot.Modules.Messaging.Application.Abstractions; +namespace Knot.Modules.TelegramImport.Application.TelegramImport; public record ExecuteImportResponseDto(bool Success, int MessagesImported, Guid ChatId); @@ -491,3 +494,7 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler + + + + + + + + + net10.0 + enable + enable + + + diff --git a/backend/src/Modules/TelegramImport/Presentation/Endpoints/TelegramImportEndpoints.cs b/backend/src/Modules/TelegramImport/Presentation/Endpoints/TelegramImportEndpoints.cs new file mode 100644 index 0000000..f135a3d --- /dev/null +++ b/backend/src/Modules/TelegramImport/Presentation/Endpoints/TelegramImportEndpoints.cs @@ -0,0 +1,40 @@ +using Carter; +using Knot.Shared.Kernel; +using Knot.Modules.TelegramImport.Application.TelegramImport.DTOs; +using Knot.Modules.TelegramImport.Application.TelegramImport; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Mvc; + +namespace Knot.Modules.TelegramImport.Presentation.Endpoints; + +public sealed class TelegramImportEndpoints : ICarterModule +{ + public void AddRoutes(IEndpointRouteBuilder app) + { + var group = app.MapGroup("api/import/telegram").RequireAuthorization(); + + group.MapPost("analyze", async (HttpRequest req, ISender sender, CancellationToken ct) => + { + if (!req.HasFormContentType) return Results.BadRequest("No file uploaded."); + var form = await req.ReadFormAsync(ct); + var file = form.Files.FirstOrDefault(); + if (file == null) return Results.BadRequest("No file uploaded."); + + using var stream = file.OpenReadStream(); + var result = await sender.Send(new AnalyzeImportCommand(stream, file.FileName), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error.Description ?? result.Error.Code); + }).DisableAntiforgery(); + + group.MapPost("execute", async ([FromBody] ExecuteImportRequest req, ISender sender, IUserContext userContext, CancellationToken ct) => + { + var command = new ExecuteImportCommand(userContext.UserId, req.Token, req.Mapping, req.GroupName); + var result = await sender.Send(command, ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error.Description ?? result.Error.Code); + }); + } +} + + diff --git a/backend/src/Host/Application/WebRtc/Queries/GetIceServersQuery.cs b/backend/src/Modules/WebRtc/Application/WebRtc/Queries/GetIceServersQuery.cs similarity index 51% rename from backend/src/Host/Application/WebRtc/Queries/GetIceServersQuery.cs rename to backend/src/Modules/WebRtc/Application/WebRtc/Queries/GetIceServersQuery.cs index 22ccb5e..ff15eea 100644 --- a/backend/src/Host/Application/WebRtc/Queries/GetIceServersQuery.cs +++ b/backend/src/Modules/WebRtc/Application/WebRtc/Queries/GetIceServersQuery.cs @@ -31,7 +31,7 @@ internal sealed class GetIceServersQueryHandler : IQueryHandler(new Error( Knot.Shared.Kernel.Constants.Errors.DisabledByAdmin, - "Сервис отключен администратором." + "Сервис отключен администратором." ))); } @@ -70,4 +70,4 @@ internal sealed class GetIceServersQueryHandler : IQueryHandler + + net10.0 + enable + enable + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/Modules/WebRtc/Presentation/Endpoints/WebRtcEndpoints.cs b/backend/src/Modules/WebRtc/Presentation/Endpoints/WebRtcEndpoints.cs new file mode 100644 index 0000000..cfff00f --- /dev/null +++ b/backend/src/Modules/WebRtc/Presentation/Endpoints/WebRtcEndpoints.cs @@ -0,0 +1,28 @@ +using Carter; +using MediatR; +using Knot.Shared.Kernel; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace Host.Endpoints; + +/// +/// Регистрация эндпоинтов WebRTC +/// +public sealed class WebRtcEndpoints : ICarterModule +{ + public void AddRoutes(IEndpointRouteBuilder app) + { + var group = app.MapGroup(Knot.Shared.Kernel.Constants.Routes.ApiWebRtc) + .RequireAuthorization(); + + group.MapGet("/ice-servers", async (ISender sender, CancellationToken ct) => + { + var result = await sender.Send(new Host.Application.WebRtc.Queries.GetIceServersQuery(), ct); + return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error); + }) + .WithName("GetIceServers") + .WithOpenApi(); + } +} \ No newline at end of file diff --git a/backend/src/Shared/Knot.Shared.Infrastructure/DependencyInjection.cs b/backend/src/Shared/Knot.Shared.Infrastructure/DependencyInjection.cs index da55b8a..cdfa80c 100644 --- a/backend/src/Shared/Knot.Shared.Infrastructure/DependencyInjection.cs +++ b/backend/src/Shared/Knot.Shared.Infrastructure/DependencyInjection.cs @@ -26,24 +26,7 @@ public static class DependencyInjection services.AddSingleton(new AesEncryptionService(masterKey)); - // Настройка MinIO (S3) - var s3Endpoint = configuration["S3_ENDPOINT"] ?? "minio:9000"; - var s3AccessKey = configuration["S3_ACCESS_KEY"] ?? "admin"; - var s3SecretKey = configuration["S3_SECRET_KEY"] ?? "KnotSuperSecretMinioPassword"; - var s3Bucket = configuration["S3_BUCKET"] ?? "knot-uploads"; - services.AddMinio(configureClient => configureClient - .WithEndpoint(s3Endpoint) - .WithCredentials(s3AccessKey, s3SecretKey) - .WithSSL(false) - .Build()); - - services.AddScoped(provider => - { - var minioClient = provider.GetRequiredService(); - var encService = provider.GetRequiredService(); - return new S3FileStorageService(minioClient, encService, s3Bucket); - }); // Настройка System Database var connectionString = configuration.GetConnectionString("DefaultConnection") diff --git a/backend/src/Shared/Knot.Shared.Infrastructure/Migrations/20260315204509_InitialSystem.Designer.cs b/backend/src/Shared/Knot.Shared.Infrastructure/Migrations/20260322191936_InitialSystem.Designer.cs similarity index 97% rename from backend/src/Shared/Knot.Shared.Infrastructure/Migrations/20260315204509_InitialSystem.Designer.cs rename to backend/src/Shared/Knot.Shared.Infrastructure/Migrations/20260322191936_InitialSystem.Designer.cs index 315f8e8..3166f5f 100644 --- a/backend/src/Shared/Knot.Shared.Infrastructure/Migrations/20260315204509_InitialSystem.Designer.cs +++ b/backend/src/Shared/Knot.Shared.Infrastructure/Migrations/20260322191936_InitialSystem.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Knot.Shared.Infrastructure.Migrations { [DbContext(typeof(SystemDbContext))] - [Migration("20260315204509_InitialSystem")] + [Migration("20260322191936_InitialSystem")] partial class InitialSystem { /// diff --git a/backend/src/Shared/Knot.Shared.Infrastructure/Migrations/20260315204509_InitialSystem.cs b/backend/src/Shared/Knot.Shared.Infrastructure/Migrations/20260322191936_InitialSystem.cs similarity index 100% rename from backend/src/Shared/Knot.Shared.Infrastructure/Migrations/20260315204509_InitialSystem.cs rename to backend/src/Shared/Knot.Shared.Infrastructure/Migrations/20260322191936_InitialSystem.cs diff --git a/backend/src/Shared/Knot.Shared.Kernel/Storage/IFileStorageService.cs b/backend/src/Shared/Knot.Shared.Kernel/Storage/IFileStorageService.cs index a898f4a..4a94c78 100644 --- a/backend/src/Shared/Knot.Shared.Kernel/Storage/IFileStorageService.cs +++ b/backend/src/Shared/Knot.Shared.Kernel/Storage/IFileStorageService.cs @@ -16,4 +16,10 @@ public interface IFileStorageService // Получает список всех файлов в хранилище с их размерами. Task> ListFilesAsync(); + + // Получает размер конкретного файла + Task GetFileSizeAsync(string fileId, System.Threading.CancellationToken ct = default); + + // Получает размеры списка файлов + Task> GetFileSizesAsync(System.Collections.Generic.IEnumerable fileIds, System.Threading.CancellationToken ct = default); } diff --git a/backend/tests/Knot.Modules.Identity.UnitTests/RegisterUserCommandHandlerTests.cs b/backend/tests/Knot.Modules.Identity.UnitTests/RegisterUserCommandHandlerTests.cs deleted file mode 100644 index efdb4d0..0000000 --- a/backend/tests/Knot.Modules.Identity.UnitTests/RegisterUserCommandHandlerTests.cs +++ /dev/null @@ -1,95 +0,0 @@ -using FluentAssertions; -using NSubstitute; -using Knot.Modules.Identity.Application.Abstractions; -using Knot.Modules.Identity.Application.Users.Register; -using Knot.Modules.Identity.Domain; -using Knot.Shared.Kernel; -using Knot.Shared.Kernel.Configuration; -using Xunit; -using System.Threading; -using System.Threading.Tasks; - -namespace Knot.Modules.Identity.UnitTests; - -public class RegisterUserCommandHandlerTests -{ - private readonly IUserRepository _userRepository; - private readonly IIdentityUnitOfWork _unitOfWork; - private readonly ISettingsService _settings; - private readonly IJwtTokenProvider _tokenProvider; - private readonly RegisterUserCommandHandler _handler; - - public RegisterUserCommandHandlerTests() - { - _userRepository = Substitute.For(); - _unitOfWork = Substitute.For(); - _settings = Substitute.For(); - _tokenProvider = Substitute.For(); - - var options = new SystemSettingsDto { EnableRegistration = true }; - _settings.Current.Returns(options); - - _handler = new RegisterUserCommandHandler(_userRepository, _unitOfWork, _settings, _tokenProvider); - } - - [Fact] - public async Task Handle_ShouldReturnSuccess_WhenRegistrationIsSuccessful() - { - // Arrange - var command = new RegisterUserCommand("testuser", "password123", "Test User", "test@example.com", "My bio"); - _userRepository.IsUsernameUniqueAsync(command.Username, Arg.Any()) - .Returns(true); - _tokenProvider.Generate(Arg.Any()).Returns("my-token"); - - // Act - var result = await _handler.Handle(command, CancellationToken.None); - - // Assert - result.IsSuccess.Should().BeTrue(); - result.Value.Token.Should().Be("my-token"); - - _userRepository.Received(1).Add(Arg.Is(u => - u.Username == command.Username && - u.DisplayName == command.DisplayName && - u.Email == command.Email)); - - await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any()); - } - - [Fact] - public async Task Handle_ShouldReturnFailure_WhenUsernameIsNotUnique() - { - // Arrange - var command = new RegisterUserCommand("duplicate", "password123", "Test User", null, null); - _userRepository.IsUsernameUniqueAsync(command.Username, Arg.Any()) - .Returns(false); - - // Act - var result = await _handler.Handle(command, CancellationToken.None); - - // Assert - result.IsFailure.Should().BeTrue(); - result.Error.Code.Should().Be(IdentityErrors.IdentityUsernameNotUnique.Code); - - _userRepository.DidNotReceive().Add(Arg.Any()); - await _unitOfWork.DidNotReceive().SaveChangesAsync(Arg.Any()); - } - - [Fact] - public async Task Handle_ShouldReturnFailure_WhenRegistrationDisabled() - { - // Arrange - _settings.Current.Returns(new SystemSettingsDto { EnableRegistration = false }); - var command = new RegisterUserCommand("testuser", "password123", "Test User", null, null); - - // Act - var result = await _handler.Handle(command, CancellationToken.None); - - // Assert - result.IsFailure.Should().BeTrue(); - result.Error.Code.Should().Be(IdentityErrors.IdentityRegistrationDisabled.Code); - - _userRepository.DidNotReceive().Add(Arg.Any()); - await _unitOfWork.DidNotReceive().SaveChangesAsync(Arg.Any()); - } -} diff --git a/client-web/src/core/infrastructure/i18n.ts b/client-web/src/core/infrastructure/i18n.ts index 6532bc3..562999c 100644 --- a/client-web/src/core/infrastructure/i18n.ts +++ b/client-web/src/core/infrastructure/i18n.ts @@ -285,6 +285,7 @@ const translations = { loadChatsError: 'Ошибка загрузки чатов', loadMessagesError: 'Ошибка загрузки сообщений', unreadMessages: 'Непрочитанные сообщения', + attachmentDiscardConfirm: 'У вас есть прикрепленные вложения. Если вы перейдете в другой чат, они будут потеряны. Продолжить?', }, en: { myProfile: 'My Profile', @@ -540,6 +541,7 @@ const translations = { loadChatsError: 'Error loading chats', loadMessagesError: 'Error loading messages', unreadMessages: 'Unread messages', + attachmentDiscardConfirm: 'You have attached files. If you switch to another chat, they will be lost. Continue?', }, } as const; diff --git a/client-web/src/core/presentation/components/ui/ImageLightbox.tsx b/client-web/src/core/presentation/components/ui/ImageLightbox.tsx index 9b57c72..d0d5bb4 100644 --- a/client-web/src/core/presentation/components/ui/ImageLightbox.tsx +++ b/client-web/src/core/presentation/components/ui/ImageLightbox.tsx @@ -13,8 +13,14 @@ interface ImageLightboxProps { export default function ImageLightbox({ url, images, initialIndex = 0, onClose }: ImageLightboxProps) { const gallery = images && images.length > 0; const [index, setIndex] = useState(initialIndex); - const currentUrl = gallery ? images![index].url : url!; + + useEffect(() => { + setIndex(initialIndex); + }, [initialIndex]); + + const rawUrl = gallery ? images![index].url : url!; const currentType = gallery ? images![index].type : undefined; + const currentUrl = currentType === 'video' ? rawUrl + (rawUrl.includes('?') ? '&lb=' + index : '?lb=' + index) : rawUrl; const total = gallery ? images!.length : 1; const goPrev = useCallback(() => { @@ -101,6 +107,7 @@ export default function ImageLightbox({ url, images, initialIndex = 0, onClose } src={currentUrl} controls autoPlay + playsInline preload="metadata" className="w-full h-full object-contain outline-none bg-black/50" /> diff --git a/client-web/src/modules/chats/presentation/components/GroupSettings.tsx b/client-web/src/modules/chats/presentation/components/GroupSettings.tsx index 25f412d..59981bf 100644 --- a/client-web/src/modules/chats/presentation/components/GroupSettings.tsx +++ b/client-web/src/modules/chats/presentation/components/GroupSettings.tsx @@ -703,9 +703,7 @@ export default function GroupSettings({ chat, onClose, onGoToMessage }: GroupSet {m.thumbnail ? ( ) : ( -
-
+